Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4994aafea | ||
|
|
13e030901a | ||
|
|
2a28a8abbd | ||
|
|
8e10244086 | ||
|
|
9b40aa3afb | ||
|
|
246c62fd49 | ||
|
|
5346d6d0c9 | ||
|
|
876eb301a0 | ||
|
|
2f56ee01b3 | ||
|
|
2bbbf1b9f1 | ||
|
|
e9f975eb02 | ||
|
|
1aaef36763 | ||
|
|
bf67b7f955 | ||
|
|
6b8352a8a0 | ||
|
|
6f0ab0cf5f | ||
|
|
5983722793 |
19
.claude/settings.local.json
Normal file
19
.claude/settings.local.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ng generate:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ng build:*)",
|
||||
"Bash(for lib in auth-client hcl-studio shared-utils ui-accessibility ui-animations ui-backgrounds ui-code-display ui-data-utils ui-design-system ui-essentials ui-font-manager ui-landing-pages)",
|
||||
"Bash(do)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(done)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
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**.
|
||||
491
CONSUMER_INTEGRATION_GUIDE.md
Normal file
491
CONSUMER_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# Angular Library Consumer Integration Guide
|
||||
|
||||
## 🚀 Complete Guide: Creating a New Angular Project with SSuite Libraries
|
||||
|
||||
This guide shows you how to create a fresh Angular project and integrate any combination of the 12 SSuite libraries using Git submodules.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- **Node.js 18+** and npm
|
||||
- **Angular CLI** (`npm install -g @angular/cli`)
|
||||
- **Git** configured with access to `git.sky-ai.com`
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Step 1: Create New Angular Project
|
||||
|
||||
```bash
|
||||
# Create new Angular project
|
||||
ng new my-awesome-app
|
||||
cd my-awesome-app
|
||||
|
||||
# Create libs directory for submodules
|
||||
mkdir libs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 2: Choose Your Libraries
|
||||
|
||||
Select libraries based on your needs:
|
||||
|
||||
### 🎨 **Foundation (Required for most projects)**
|
||||
- **ui-design-system** - SCSS design tokens, colors, typography, fonts
|
||||
|
||||
### 🛠️ **Essential Components**
|
||||
- **ui-essentials** - Buttons, forms, tables, cards, navigation, etc.
|
||||
- **shared-utils** - Common utilities and shared services
|
||||
|
||||
### 🎯 **Specialized Libraries**
|
||||
- **auth-client** - Authentication guards, services, interceptors
|
||||
- **ui-landing-pages** - Hero sections, pricing tables, testimonials
|
||||
- **ui-code-display** - Syntax highlighting for code blocks
|
||||
- **ui-accessibility** - WCAG compliance tools and utilities
|
||||
- **hcl-studio** - Advanced color management system
|
||||
|
||||
### 🎭 **Enhancement Libraries**
|
||||
- **ui-animations** - CSS animation utilities
|
||||
- **ui-backgrounds** - Background generators and effects
|
||||
- **ui-font-manager** - Advanced font management
|
||||
- **ui-data-utils** - Data manipulation utilities
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Step 3: Add Libraries as Submodules
|
||||
|
||||
**IMPORTANT**: Always add `ui-design-system` first, as other libraries depend on it.
|
||||
|
||||
### Minimal Setup (Design System + Components):
|
||||
```bash
|
||||
# Add foundation (required)
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
|
||||
# Add essential components
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://git.sky-ai.com/jules/shared-utils.git libs/shared-utils
|
||||
|
||||
# Initialize submodules
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Full-Featured Setup (All Libraries):
|
||||
```bash
|
||||
# Foundation (required first)
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
|
||||
# Core libraries
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://git.sky-ai.com/jules/shared-utils.git libs/shared-utils
|
||||
git submodule add https://git.sky-ai.com/jules/auth-client.git libs/auth-client
|
||||
|
||||
# Specialized libraries
|
||||
git submodule add https://git.sky-ai.com/jules/ui-landing-pages.git libs/ui-landing-pages
|
||||
git submodule add https://git.sky-ai.com/jules/ui-code-display.git libs/ui-code-display
|
||||
git submodule add https://git.sky-ai.com/jules/ui-accessibility.git libs/ui-accessibility
|
||||
git submodule add https://git.sky-ai.com/jules/hcl-studio.git libs/hcl-studio
|
||||
|
||||
# Enhancement libraries
|
||||
git submodule add https://git.sky-ai.com/jules/ui-animations.git libs/ui-animations
|
||||
git submodule add https://git.sky-ai.com/jules/ui-backgrounds.git libs/ui-backgrounds
|
||||
git submodule add https://git.sky-ai.com/jules/ui-font-manager.git libs/ui-font-manager
|
||||
git submodule add https://git.sky-ai.com/jules/ui-data-utils.git libs/ui-data-utils
|
||||
|
||||
# Initialize all submodules
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔨 Step 4: Build Libraries
|
||||
|
||||
Build libraries in dependency order:
|
||||
|
||||
```bash
|
||||
# Build ui-design-system first (required by others)
|
||||
cd libs/ui-design-system
|
||||
npm install
|
||||
npm run build
|
||||
cd ../..
|
||||
|
||||
# Build other libraries
|
||||
cd libs/ui-essentials && npm install && npm run build && cd ../..
|
||||
cd libs/shared-utils && npm install && npm run build && cd ../..
|
||||
cd libs/auth-client && npm install && npm run build && cd ../..
|
||||
|
||||
# Continue for other libraries as needed...
|
||||
```
|
||||
|
||||
### 📜 Automation Script:
|
||||
Create `build-libs.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "🏗️ Building all libraries..."
|
||||
|
||||
# Array of libraries in dependency order
|
||||
LIBS=("ui-design-system" "shared-utils" "ui-essentials" "auth-client" "ui-landing-pages" "ui-code-display" "ui-accessibility" "hcl-studio" "ui-animations" "ui-backgrounds" "ui-font-manager" "ui-data-utils")
|
||||
|
||||
for lib in "${LIBS[@]}"; do
|
||||
if [ -d "libs/$lib" ]; then
|
||||
echo "📦 Building $lib..."
|
||||
cd libs/$lib
|
||||
npm install
|
||||
npm run build
|
||||
cd ../..
|
||||
echo "✅ $lib built successfully"
|
||||
else
|
||||
echo "⚠️ Skipping $lib (not found)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "🎉 All libraries built!"
|
||||
```
|
||||
|
||||
Make executable: `chmod +x build-libs.sh && ./build-libs.sh`
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 5: Configure TypeScript Paths
|
||||
|
||||
Update `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
// Foundation (required)
|
||||
"ui-design-system": ["./libs/ui-design-system/dist"],
|
||||
"ui-design-system/*": ["./libs/ui-design-system/dist/*", "./libs/ui-design-system/src/*"],
|
||||
|
||||
// Core libraries
|
||||
"ui-essentials": ["./libs/ui-essentials/dist"],
|
||||
"ui-essentials/*": ["./libs/ui-essentials/dist/*"],
|
||||
"shared-utils": ["./libs/shared-utils/dist"],
|
||||
"shared-utils/*": ["./libs/shared-utils/dist/*"],
|
||||
"auth-client": ["./libs/auth-client/dist"],
|
||||
"auth-client/*": ["./libs/auth-client/dist/*"],
|
||||
|
||||
// Specialized libraries
|
||||
"ui-landing-pages": ["./libs/ui-landing-pages/dist"],
|
||||
"ui-landing-pages/*": ["./libs/ui-landing-pages/dist/*"],
|
||||
"ui-code-display": ["./libs/ui-code-display/dist"],
|
||||
"ui-code-display/*": ["./libs/ui-code-display/dist/*"],
|
||||
"ui-accessibility": ["./libs/ui-accessibility/dist"],
|
||||
"ui-accessibility/*": ["./libs/ui-accessibility/dist/*"],
|
||||
"hcl-studio": ["./libs/hcl-studio/dist"],
|
||||
"hcl-studio/*": ["./libs/hcl-studio/dist/*"],
|
||||
|
||||
// Enhancement libraries
|
||||
"ui-animations": ["./libs/ui-animations/dist"],
|
||||
"ui-animations/*": ["./libs/ui-animations/dist/*"],
|
||||
"ui-backgrounds": ["./libs/ui-backgrounds/dist"],
|
||||
"ui-backgrounds/*": ["./libs/ui-backgrounds/dist/*"],
|
||||
"ui-font-manager": ["./libs/ui-font-manager/dist"],
|
||||
"ui-font-manager/*": ["./libs/ui-font-manager/dist/*"],
|
||||
"ui-data-utils": ["./libs/ui-data-utils/dist"],
|
||||
"ui-data-utils/*": ["./libs/ui-data-utils/dist/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `ui-design-system/*` includes both `dist/*` and `src/*` to support SCSS imports.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Step 6: Import Styles
|
||||
|
||||
### Option A: Complete Design System
|
||||
In your `src/styles.scss`:
|
||||
```scss
|
||||
// Complete design system (includes all tokens + fonts)
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// Optional: Additional component styles
|
||||
@use 'ui-animations/src/styles' as animations;
|
||||
@use 'ui-backgrounds/src/styles' as backgrounds;
|
||||
```
|
||||
|
||||
### Option B: Selective Imports
|
||||
```scss
|
||||
// Just semantic tokens (most common)
|
||||
@use 'ui-design-system/src/styles/semantic' as tokens;
|
||||
|
||||
// Or just base tokens
|
||||
@use 'ui-design-system/src/styles/base' as base;
|
||||
|
||||
// Add specific libraries
|
||||
@use 'ui-accessibility/src/lib/utilities/a11y-utilities' as a11y;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Step 7: Use Libraries in Your Components
|
||||
|
||||
### Example Component using multiple libraries:
|
||||
|
||||
```typescript
|
||||
// src/app/app.component.ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { AuthService } from 'auth-client';
|
||||
import { HeroSectionComponent } from 'ui-landing-pages';
|
||||
import { CodeSnippetComponent } from 'ui-code-display';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ButtonComponent,
|
||||
HeroSectionComponent,
|
||||
CodeSnippetComponent
|
||||
],
|
||||
template: `
|
||||
<div class="app-container">
|
||||
<!-- Hero Section -->
|
||||
<ui-hero-section
|
||||
title="Welcome to My App"
|
||||
subtitle="Built with SSuite Libraries"
|
||||
[cta]="{ text: 'Get Started', action: 'primary' }">
|
||||
</ui-hero-section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<main class="content">
|
||||
<!-- Buttons from ui-essentials -->
|
||||
<ui-button variant="primary" (click)="handleClick()">
|
||||
Primary Action
|
||||
</ui-button>
|
||||
|
||||
<ui-button variant="secondary" [disabled]="!isLoggedIn">
|
||||
Secondary Action
|
||||
</ui-button>
|
||||
|
||||
<!-- Code Display -->
|
||||
<ui-code-snippet
|
||||
language="typescript"
|
||||
[code]="exampleCode">
|
||||
</ui-code-snippet>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.content {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class AppComponent {
|
||||
isLoggedIn = false;
|
||||
exampleCode = `
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { AuthService } from 'auth-client';
|
||||
|
||||
// Your code here...
|
||||
`;
|
||||
|
||||
constructor(private auth: AuthService) {
|
||||
this.isLoggedIn = this.auth.isAuthenticated();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
console.log('Button clicked!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with Authentication:
|
||||
|
||||
```typescript
|
||||
// src/app/protected/dashboard.component.ts
|
||||
import { Component } from '@angular/core';
|
||||
import { AuthGuard } from 'auth-client';
|
||||
import { TableComponent, CardComponent } from 'ui-essentials';
|
||||
import { StatisticsDisplayComponent } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [TableComponent, CardComponent, StatisticsDisplayComponent],
|
||||
template: `
|
||||
<div class="dashboard">
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<ui-statistics-display
|
||||
[statistics]="stats"
|
||||
variant="cards">
|
||||
</ui-statistics-display>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<ui-card>
|
||||
<ui-enhanced-table
|
||||
[data]="userData"
|
||||
[columns]="tableColumns"
|
||||
[searchable]="true"
|
||||
[sortable]="true">
|
||||
</ui-enhanced-table>
|
||||
</ui-card>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.dashboard {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.stats-grid {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class DashboardComponent {
|
||||
stats = [
|
||||
{ label: 'Users', value: '1,234', change: '+12%' },
|
||||
{ label: 'Revenue', value: '$45,678', change: '+8%' },
|
||||
{ label: 'Orders', value: '892', change: '+15%' }
|
||||
];
|
||||
|
||||
userData = [
|
||||
{ id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
|
||||
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' }
|
||||
];
|
||||
|
||||
tableColumns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status' }
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step 8: Library Management
|
||||
|
||||
### Update a Single Library:
|
||||
```bash
|
||||
cd libs/ui-essentials
|
||||
git pull origin main
|
||||
npm run build
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### Update All Libraries:
|
||||
```bash
|
||||
git submodule update --remote
|
||||
./build-libs.sh # Rebuild all libraries
|
||||
```
|
||||
|
||||
### Lock Library to Specific Version:
|
||||
```bash
|
||||
cd libs/ui-essentials
|
||||
git checkout v1.2.0 # Specific tag
|
||||
cd ../..
|
||||
git add libs/ui-essentials
|
||||
git commit -m "Lock ui-essentials to v1.2.0"
|
||||
```
|
||||
|
||||
### Remove Unused Library:
|
||||
```bash
|
||||
git submodule deinit libs/ui-backgrounds
|
||||
git rm libs/ui-backgrounds
|
||||
git commit -m "Remove ui-backgrounds library"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Final Project Structure
|
||||
|
||||
```
|
||||
my-awesome-app/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── app.component.ts
|
||||
│ │ ├── protected/
|
||||
│ │ │ └── dashboard.component.ts
|
||||
│ │ └── ...
|
||||
│ ├── styles.scss
|
||||
│ └── ...
|
||||
├── libs/ # Git submodules
|
||||
│ ├── ui-design-system/ # Foundation
|
||||
│ ├── ui-essentials/ # Components
|
||||
│ ├── auth-client/ # Authentication
|
||||
│ ├── ui-landing-pages/ # Landing components
|
||||
│ └── [other-libraries]/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── angular.json
|
||||
├── build-libs.sh # Build automation
|
||||
├── .gitmodules # Auto-generated
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick Start Commands
|
||||
|
||||
### Minimal Project (Design System + Components):
|
||||
```bash
|
||||
ng new my-app && cd my-app
|
||||
mkdir libs
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule update --init --recursive
|
||||
cd libs/ui-design-system && npm install && npm run build && cd ../..
|
||||
cd libs/ui-essentials && npm install && npm run build && cd ../..
|
||||
```
|
||||
|
||||
### Add TypeScript paths and styles, then start developing!
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Common Patterns
|
||||
|
||||
### Landing Page Project:
|
||||
```bash
|
||||
# Essential for marketing sites
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://git.sky-ai.com/jules/ui-landing-pages.git libs/ui-landing-pages
|
||||
git submodule add https://git.sky-ai.com/jules/ui-animations.git libs/ui-animations
|
||||
```
|
||||
|
||||
### Documentation Site:
|
||||
```bash
|
||||
# Perfect for docs with code examples
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://git.sky-ai.com/jules/ui-code-display.git libs/ui-code-display
|
||||
git submodule add https://git.sky-ai.com/jules/ui-accessibility.git libs/ui-accessibility
|
||||
```
|
||||
|
||||
### Enterprise Application:
|
||||
```bash
|
||||
# Full-featured business app
|
||||
git submodule add https://git.sky-ai.com/jules/ui-design-system.git libs/ui-design-system
|
||||
git submodule add https://git.sky-ai.com/jules/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://git.sky-ai.com/jules/shared-utils.git libs/shared-utils
|
||||
git submodule add https://git.sky-ai.com/jules/auth-client.git libs/auth-client
|
||||
git submodule add https://git.sky-ai.com/jules/ui-data-utils.git libs/ui-data-utils
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 You're Ready!
|
||||
|
||||
Your new Angular project is now set up with the SSuite library ecosystem. You have:
|
||||
|
||||
- ✅ **Modular architecture** - Only include libraries you need
|
||||
- ✅ **Professional design system** - Consistent styling and components
|
||||
- ✅ **Independent updates** - Update libraries separately
|
||||
- ✅ **Clean imports** - TypeScript paths configured
|
||||
- ✅ **Version control** - Lock libraries to specific versions
|
||||
- ✅ **Scalable structure** - Add/remove libraries as needed
|
||||
|
||||
Happy coding! 🎉
|
||||
978
CUSTOMIZATION_AND_THEMING_GUIDE.md
Normal file
978
CUSTOMIZATION_AND_THEMING_GUIDE.md
Normal file
@@ -0,0 +1,978 @@
|
||||
# SSuite Customization and Theming Guide
|
||||
|
||||
## 🎨 Complete Guide to Styling and Overriding SSuite Libraries
|
||||
|
||||
This guide shows you how to customize and theme your Angular project using the SSuite library ecosystem. Learn how to override colors, typography, components, and create your own design system on top of the foundation.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
Before customizing, ensure you have:
|
||||
- **SSuite libraries integrated** (follow `CONSUMER_INTEGRATION_GUIDE.md`)
|
||||
- **ui-design-system** added as a submodule (foundation library)
|
||||
- **Basic SCSS knowledge** for advanced customizations
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Customization Architecture
|
||||
|
||||
SSuite uses a **layered architecture** for maximum flexibility:
|
||||
|
||||
```
|
||||
Your Custom Theme
|
||||
↓
|
||||
Semantic Tokens ← Override here for colors, typography, spacing
|
||||
↓
|
||||
Base Tokens ← Override here for foundational values
|
||||
↓
|
||||
Components ← Override here for specific component styling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Approach 1: CSS Custom Properties (Recommended)
|
||||
|
||||
The easiest way to customize SSuite libraries using CSS variables.
|
||||
|
||||
### 1. Create Your Theme File
|
||||
|
||||
Create `src/styles/theme.scss`:
|
||||
|
||||
```scss
|
||||
// ==========================================================================
|
||||
// YOUR CUSTOM THEME
|
||||
// ==========================================================================
|
||||
|
||||
// Import design system first
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// ==========================================================================
|
||||
// 🎨 COLOR OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
:root {
|
||||
// Primary Brand Colors
|
||||
--color-primary: #6366f1; // Your brand primary (indigo)
|
||||
--color-secondary: #8b5cf6; // Your brand secondary (purple)
|
||||
--color-tertiary: #10b981; // Your accent color (emerald)
|
||||
--color-error: #ef4444; // Error/danger color (red)
|
||||
|
||||
// Primary color variations
|
||||
--color-primary-50: #eef2ff;
|
||||
--color-primary-100: #e0e7ff;
|
||||
--color-primary-500: #6366f1; // Main primary
|
||||
--color-primary-600: #4f46e5;
|
||||
--color-primary-700: #4338ca;
|
||||
--color-primary-900: #312e81;
|
||||
|
||||
// Surface colors (backgrounds)
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-variant: #f8fafc;
|
||||
--color-surface-container: #f1f5f9;
|
||||
--color-surface-dim: #e2e8f0;
|
||||
|
||||
// Text colors on your custom colors
|
||||
--color-on-primary: #ffffff;
|
||||
--color-on-secondary: #ffffff;
|
||||
--color-on-surface: #1e293b;
|
||||
|
||||
// ==========================================================================
|
||||
// ✍️ TYPOGRAPHY OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Font families (use your own fonts)
|
||||
--font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--font-family-display: 'Playfair Display', Georgia, serif;
|
||||
|
||||
// Font sizes
|
||||
--font-size-xs: 0.75rem; // 12px
|
||||
--font-size-sm: 0.875rem; // 14px
|
||||
--font-size-base: 1rem; // 16px
|
||||
--font-size-lg: 1.125rem; // 18px
|
||||
--font-size-xl: 1.25rem; // 20px
|
||||
--font-size-2xl: 1.5rem; // 24px
|
||||
--font-size-3xl: 1.875rem; // 30px
|
||||
--font-size-4xl: 2.25rem; // 36px
|
||||
|
||||
// ==========================================================================
|
||||
// 📏 SPACING & SIZING OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Border radius (adjust roundness)
|
||||
--border-radius-sm: 0.25rem; // 4px - subtle
|
||||
--border-radius-base: 0.5rem; // 8px - default
|
||||
--border-radius-lg: 0.75rem; // 12px - rounded
|
||||
--border-radius-xl: 1rem; // 16px - very rounded
|
||||
|
||||
// Component-specific spacing
|
||||
--spacing-button-padding-x: 1.5rem; // Button horizontal padding
|
||||
--spacing-button-padding-y: 0.75rem; // Button vertical padding
|
||||
--spacing-card-padding: 1.5rem; // Card interior padding
|
||||
--spacing-section: 4rem; // Section spacing
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Import Your Theme
|
||||
|
||||
In `src/styles.scss`:
|
||||
|
||||
```scss
|
||||
// Import your custom theme (this sets CSS variables)
|
||||
@import 'styles/theme';
|
||||
|
||||
// Global styles using your theme
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
// Apply theme to headings
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-family-display);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
// Custom link styling
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Approach 2: SCSS Variable Overrides (Advanced)
|
||||
|
||||
For deeper customization, override SCSS variables before importing SSuite.
|
||||
|
||||
### 1. Create Advanced Theme File
|
||||
|
||||
Create `src/styles/advanced-theme.scss`:
|
||||
|
||||
```scss
|
||||
// ==========================================================================
|
||||
// ADVANCED SCSS VARIABLE OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// ==========================================================================
|
||||
// 🎨 COLOR VARIABLE OVERRIDES (before importing ui-design-system)
|
||||
// ==========================================================================
|
||||
|
||||
// Override semantic colors
|
||||
$semantic-color-brand-primary: #7c3aed !default; // Purple brand
|
||||
$semantic-color-brand-secondary: #059669 !default; // Emerald secondary
|
||||
$semantic-color-brand-accent: #dc2626 !default; // Red accent
|
||||
|
||||
// Override feedback colors
|
||||
$semantic-color-success: #10b981 !default; // Custom green
|
||||
$semantic-color-warning: #f59e0b !default; // Custom amber
|
||||
$semantic-color-danger: #ef4444 !default; // Custom red
|
||||
$semantic-color-info: #3b82f6 !default; // Custom blue
|
||||
|
||||
// Override surface colors
|
||||
$semantic-color-surface-primary: #ffffff !default; // Pure white
|
||||
$semantic-color-surface-secondary: #f9fafb !default; // Off-white
|
||||
$semantic-color-surface-elevated: #f3f4f6 !default; // Light gray
|
||||
|
||||
// ==========================================================================
|
||||
// ✍️ TYPOGRAPHY VARIABLE OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Font families
|
||||
$base-typography-font-family-sans: 'Inter', sans-serif !default;
|
||||
$base-typography-font-family-mono: 'JetBrains Mono', monospace !default;
|
||||
$base-typography-font-family-display: 'Playfair Display', serif !default;
|
||||
|
||||
// Font sizes
|
||||
$base-typography-font-size-xs: 0.75rem !default; // 12px
|
||||
$base-typography-font-size-sm: 0.875rem !default; // 14px
|
||||
$base-typography-font-size-base: 1rem !default; // 16px
|
||||
$base-typography-font-size-lg: 1.125rem !default; // 18px
|
||||
$base-typography-font-size-xl: 1.25rem !default; // 20px
|
||||
$base-typography-font-size-2xl: 1.5rem !default; // 24px
|
||||
$base-typography-font-size-3xl: 1.875rem !default; // 30px
|
||||
$base-typography-font-size-4xl: 2.25rem !default; // 36px
|
||||
|
||||
// Line heights
|
||||
$base-typography-line-height-tight: 1.25 !default;
|
||||
$base-typography-line-height-snug: 1.375 !default;
|
||||
$base-typography-line-height-normal: 1.5 !default;
|
||||
$base-typography-line-height-relaxed: 1.625 !default;
|
||||
|
||||
// ==========================================================================
|
||||
// 📏 SPACING & LAYOUT OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Border radius
|
||||
$semantic-border-button-radius: 0.75rem !default; // Rounded buttons
|
||||
$semantic-border-card-radius: 1rem !default; // Very rounded cards
|
||||
$semantic-border-input-radius: 0.5rem !default; // Moderate inputs
|
||||
|
||||
// Spacing values
|
||||
$semantic-spacing-section-padding: 5rem !default; // Generous sections
|
||||
$semantic-spacing-card-padding: 2rem !default; // Spacious cards
|
||||
$semantic-spacing-interactive-button-padding-x: 2rem !default; // Wide buttons
|
||||
$semantic-spacing-interactive-button-padding-y: 0.875rem !default; // Tall buttons
|
||||
|
||||
// NOW import ui-design-system (it will use your overridden variables)
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
```
|
||||
|
||||
### 2. Use Advanced Theme
|
||||
|
||||
In `src/styles.scss`:
|
||||
|
||||
```scss
|
||||
// Import your advanced theme
|
||||
@import 'styles/advanced-theme';
|
||||
|
||||
// Global styles
|
||||
body {
|
||||
font-family: ui.$base-typography-font-family-sans;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Custom component styles
|
||||
.custom-hero {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
var(--color-primary),
|
||||
var(--color-secondary)
|
||||
);
|
||||
color: white;
|
||||
padding: var(--spacing-section);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Approach 3: Component-Specific Overrides
|
||||
|
||||
Target specific SSuite components for surgical customizations.
|
||||
|
||||
### 1. Override Button Components
|
||||
|
||||
Create `src/styles/component-overrides.scss`:
|
||||
|
||||
```scss
|
||||
// ==========================================================================
|
||||
// COMPONENT-SPECIFIC OVERRIDES
|
||||
// ==========================================================================
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// ==========================================================================
|
||||
// 🔘 BUTTON OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Override all buttons
|
||||
ui-button, .ui-button {
|
||||
// Custom button base
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Custom hover effects
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Primary button overrides
|
||||
ui-button[variant="primary"], .ui-button--primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary button overrides
|
||||
ui-button[variant="secondary"], .ui-button--secondary {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// Outline button overrides
|
||||
ui-button[variant="outline"], .ui-button--outline {
|
||||
border: 2px solid currentColor;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 📋 FORM OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Input field overrides
|
||||
ui-text-input, .ui-text-input {
|
||||
input {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: var(--font-size-base);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form field overrides
|
||||
ui-form-field, .ui-form-field {
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 🃏 CARD OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
ui-card, .ui-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--border-radius-xl);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(31, 38, 135, 0.37),
|
||||
0 4px 16px rgba(31, 38, 135, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 16px 64px rgba(31, 38, 135, 0.4),
|
||||
0 8px 32px rgba(31, 38, 135, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 🚀 LANDING PAGE OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Hero section overrides
|
||||
ui-hero-section, .ui-hero-section {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(102, 126, 234, 0.9) 0%,
|
||||
rgba(118, 75, 162, 0.9) 100%
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.hero-title {
|
||||
font-family: var(--font-family-display);
|
||||
background: linear-gradient(45deg, #fff, #e2e8f0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
|
||||
// Feature grid overrides
|
||||
ui-feature-grid, .ui-feature-grid {
|
||||
.feature-item {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Component Overrides
|
||||
|
||||
In `src/styles.scss`:
|
||||
|
||||
```scss
|
||||
// Import design system
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// Import your component overrides
|
||||
@import 'styles/component-overrides';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Approach 4: Dark Mode Implementation
|
||||
|
||||
Create a comprehensive dark mode theme.
|
||||
|
||||
### 1. Create Dark Mode Theme
|
||||
|
||||
Create `src/styles/dark-theme.scss`:
|
||||
|
||||
```scss
|
||||
// ==========================================================================
|
||||
// DARK MODE THEME
|
||||
// ==========================================================================
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// Dark mode CSS variables
|
||||
[data-theme="dark"] {
|
||||
// ==========================================================================
|
||||
// 🌙 DARK MODE COLORS
|
||||
// ==========================================================================
|
||||
|
||||
// Primary colors (slightly brighter for dark mode)
|
||||
--color-primary: #818cf8; // Lighter indigo
|
||||
--color-secondary: #a78bfa; // Lighter purple
|
||||
--color-tertiary: #34d399; // Lighter emerald
|
||||
|
||||
// Dark surfaces
|
||||
--color-surface: #0f172a; // Very dark blue
|
||||
--color-surface-variant: #1e293b; // Dark blue-gray
|
||||
--color-surface-container: #334155; // Medium blue-gray
|
||||
--color-surface-elevated: #475569; // Light blue-gray
|
||||
--color-surface-dim: #64748b; // Muted blue-gray
|
||||
|
||||
// Dark mode text colors
|
||||
--color-on-surface: #f8fafc; // Light text on dark
|
||||
--color-on-primary: #1e1b4b; // Dark text on light primary
|
||||
--color-on-secondary: #2d1b69; // Dark text on light secondary
|
||||
|
||||
// Border colors for dark mode
|
||||
--color-border: #374151;
|
||||
--color-border-light: #4b5563;
|
||||
|
||||
// ==========================================================================
|
||||
// 🌙 DARK MODE COMPONENT OVERRIDES
|
||||
// ==========================================================================
|
||||
|
||||
// Card styling in dark mode
|
||||
ui-card, .ui-card {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-color: rgba(71, 85, 105, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: rgba(51, 65, 85, 0.8);
|
||||
border-color: rgba(71, 85, 105, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Input styling in dark mode
|
||||
ui-text-input input, .ui-text-input input {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-on-surface);
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: rgba(30, 41, 59, 1);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// Button adjustments for dark mode
|
||||
ui-button[variant="secondary"], .ui-button--secondary {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Dark Mode Toggle Component
|
||||
|
||||
Create a theme switcher:
|
||||
|
||||
```typescript
|
||||
// src/app/components/theme-toggle.component.ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemeSwitcherComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-toggle',
|
||||
standalone: true,
|
||||
imports: [ThemeSwitcherComponent],
|
||||
template: `
|
||||
<ui-theme-switcher
|
||||
[themes]="themes"
|
||||
(themeChange)="onThemeChange($event)">
|
||||
</ui-theme-switcher>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ThemeToggleComponent {
|
||||
themes = [
|
||||
{ id: 'light', name: 'Light', icon: '☀️' },
|
||||
{ id: 'dark', name: 'Dark', icon: '🌙' }
|
||||
];
|
||||
|
||||
onThemeChange(theme: string) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Initialize Theme
|
||||
|
||||
In `src/app/app.component.ts`:
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ThemeToggleComponent } from './components/theme-toggle.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [ThemeToggleComponent],
|
||||
template: `
|
||||
<div class="app">
|
||||
<app-theme-toggle></app-theme-toggle>
|
||||
<!-- Your app content -->
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
// Initialize theme from localStorage
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Approach 5: Custom Brand Components
|
||||
|
||||
Create your own branded components on top of SSuite.
|
||||
|
||||
### 1. Custom Branded Button
|
||||
|
||||
Create `src/app/components/brand-button.component.ts`:
|
||||
|
||||
```typescript
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-brand-button',
|
||||
standalone: true,
|
||||
imports: [ButtonComponent],
|
||||
template: `
|
||||
<ui-button
|
||||
[variant]="variant"
|
||||
[size]="size"
|
||||
[disabled]="disabled"
|
||||
class="brand-button"
|
||||
[class.gradient]="gradient"
|
||||
[class.glass]="glass">
|
||||
<ng-content></ng-content>
|
||||
</ui-button>
|
||||
`,
|
||||
styles: [`
|
||||
.brand-button {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Gradient effect
|
||||
&.gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Glass morphism effect
|
||||
&.glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class BrandButtonComponent {
|
||||
@Input() variant: 'primary' | 'secondary' | 'outline' = 'primary';
|
||||
@Input() size: 'sm' | 'md' | 'lg' = 'md';
|
||||
@Input() disabled = false;
|
||||
@Input() gradient = false;
|
||||
@Input() glass = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Custom Hero Section
|
||||
|
||||
Create `src/app/components/brand-hero.component.ts`:
|
||||
|
||||
```typescript
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { HeroSectionComponent } from 'ui-landing-pages';
|
||||
import { BrandButtonComponent } from './brand-button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-brand-hero',
|
||||
standalone: true,
|
||||
imports: [HeroSectionComponent, BrandButtonComponent],
|
||||
template: `
|
||||
<div class="brand-hero">
|
||||
<div class="hero-background"></div>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">{{ title }}</h1>
|
||||
<p class="hero-subtitle">{{ subtitle }}</p>
|
||||
<div class="hero-actions">
|
||||
<app-brand-button
|
||||
[gradient]="true"
|
||||
size="lg">
|
||||
{{ ctaText }}
|
||||
</app-brand-button>
|
||||
<app-brand-button
|
||||
[glass]="true"
|
||||
variant="outline"
|
||||
size="lg">
|
||||
{{ secondaryCta }}
|
||||
</app-brand-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.brand-hero {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="1" fill="rgba(255,255,255,0.1)"/></svg>') repeat;
|
||||
animation: float 20s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0) rotate(0deg); }
|
||||
100% { transform: translateY(-100px) rotate(360deg); }
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||
font-weight: 900;
|
||||
margin-bottom: 1.5rem;
|
||||
background: linear-gradient(45deg, #fff, #e2e8f0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: clamp(1.125rem, 3vw, 1.5rem);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 3rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class BrandHeroComponent {
|
||||
@Input() title = 'Welcome to Our Platform';
|
||||
@Input() subtitle = 'Create amazing experiences with our design system';
|
||||
@Input() ctaText = 'Get Started';
|
||||
@Input() secondaryCta = 'Learn More';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Real-World Examples
|
||||
|
||||
### Example 1: E-commerce Theme
|
||||
|
||||
```scss
|
||||
// src/styles/ecommerce-theme.scss
|
||||
:root {
|
||||
// E-commerce focused colors
|
||||
--color-primary: #059669; // Forest green (trust)
|
||||
--color-secondary: #dc2626; // Red (urgency/sales)
|
||||
--color-success: #10b981; // Success green
|
||||
--color-warning: #f59e0b; // Warning amber
|
||||
|
||||
// Product-focused typography
|
||||
--font-family-sans: 'Open Sans', sans-serif;
|
||||
--font-family-display: 'Poppins', sans-serif;
|
||||
|
||||
// E-commerce spacing
|
||||
--spacing-product-card: 1.25rem;
|
||||
--spacing-checkout: 2rem;
|
||||
|
||||
// Trust-building shadows
|
||||
--shadow-product-card: 0 4px 12px rgba(0,0,0,0.1);
|
||||
--shadow-button-primary: 0 4px 14px rgba(5,150,105,0.3);
|
||||
}
|
||||
|
||||
// Product card styling
|
||||
.product-card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-product-card);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: SaaS Platform Theme
|
||||
|
||||
```scss
|
||||
// src/styles/saas-theme.scss
|
||||
:root {
|
||||
// SaaS professional colors
|
||||
--color-primary: #3b82f6; // Professional blue
|
||||
--color-secondary: #6366f1; // Indigo accent
|
||||
--color-success: #10b981; // Success green
|
||||
|
||||
// Dashboard-focused spacing
|
||||
--spacing-sidebar: 16rem;
|
||||
--spacing-dashboard-padding: 2rem;
|
||||
--spacing-card-gap: 1.5rem;
|
||||
|
||||
// Modern typography
|
||||
--font-family-sans: 'Inter', sans-serif;
|
||||
--font-size-dashboard-title: 2rem;
|
||||
--font-size-widget-title: 1.25rem;
|
||||
}
|
||||
|
||||
// Dashboard layout
|
||||
.dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--spacing-sidebar) 1fr;
|
||||
min-height: 100vh;
|
||||
|
||||
.sidebar {
|
||||
background: var(--color-surface-variant);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--spacing-dashboard-padding);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Advanced Customization Tips
|
||||
|
||||
### 1. Use CSS Custom Properties for Runtime Changes
|
||||
|
||||
```typescript
|
||||
// Dynamically change theme colors
|
||||
export class ThemeService {
|
||||
setThemeColor(property: string, value: string) {
|
||||
document.documentElement.style.setProperty(`--${property}`, value);
|
||||
}
|
||||
|
||||
// Example: User customization
|
||||
applyUserTheme(userTheme: UserTheme) {
|
||||
this.setThemeColor('color-primary', userTheme.primaryColor);
|
||||
this.setThemeColor('font-family-sans', userTheme.fontFamily);
|
||||
this.setThemeColor('border-radius-base', userTheme.borderRadius);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Component-Level CSS Custom Properties
|
||||
|
||||
```scss
|
||||
ui-button {
|
||||
// Component-specific custom properties
|
||||
--btn-padding-x: 2rem;
|
||||
--btn-font-weight: 700;
|
||||
--btn-border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
// Modify specific button instances
|
||||
.hero-button {
|
||||
--btn-padding-x: 3rem;
|
||||
--btn-font-size: 1.25rem;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Responsive Theme Adjustments
|
||||
|
||||
```scss
|
||||
:root {
|
||||
--font-size-hero: 2.5rem;
|
||||
--spacing-section: 3rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--font-size-hero: 4rem;
|
||||
--spacing-section: 5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Best Practices
|
||||
|
||||
### 1. **Layer Your Customizations**
|
||||
- Start with CSS custom properties for quick changes
|
||||
- Use SCSS variables for deeper customizations
|
||||
- Create component overrides for specific needs
|
||||
|
||||
### 2. **Maintain Consistency**
|
||||
- Define a clear color palette
|
||||
- Use consistent spacing scales
|
||||
- Maintain typography hierarchy
|
||||
|
||||
### 3. **Test Across Components**
|
||||
- Ensure your theme works across all SSuite components
|
||||
- Test in both light and dark modes
|
||||
- Verify accessibility (contrast ratios)
|
||||
|
||||
### 4. **Performance Considerations**
|
||||
- Use CSS custom properties for dynamic changes
|
||||
- Avoid excessive nesting in component overrides
|
||||
- Minimize redundant style declarations
|
||||
|
||||
### 5. **Documentation**
|
||||
- Document your theme variables
|
||||
- Create a style guide for your team
|
||||
- Maintain examples of themed components
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start Templates
|
||||
|
||||
### Minimal Customization (5 minutes)
|
||||
```scss
|
||||
// src/styles.scss
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
:root {
|
||||
--color-primary: #your-brand-color;
|
||||
--font-family-sans: 'Your Font', sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Medium Customization (30 minutes)
|
||||
- Use Approach 1 (CSS Custom Properties)
|
||||
- Add dark mode support
|
||||
- Create 2-3 component overrides
|
||||
|
||||
### Full Customization (2+ hours)
|
||||
- Use Approach 2 (SCSS Variable Overrides)
|
||||
- Implement comprehensive dark mode
|
||||
- Create branded components
|
||||
- Add responsive theme adjustments
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're Ready to Theme!
|
||||
|
||||
With this guide, you can create beautiful, branded experiences using the SSuite library ecosystem. Start small with CSS custom properties and gradually add more advanced customizations as needed.
|
||||
|
||||
Your themed application will maintain the robust functionality of SSuite components while expressing your unique brand identity! 🎨✨
|
||||
1137
HCL_STUDIO_USAGE_GUIDE.md
Normal file
1137
HCL_STUDIO_USAGE_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
138
LIBRARY_EXTRACTION_PLAN.md
Normal file
138
LIBRARY_EXTRACTION_PLAN.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Library Extraction Plan
|
||||
|
||||
## Overview
|
||||
Convert current Angular workspace with 12 libraries into individual Git repositories for submodule distribution.
|
||||
|
||||
## Dependencies Analysis
|
||||
|
||||
### Core Libraries (No internal dependencies)
|
||||
1. **ui-design-system** - Foundation styles and tokens
|
||||
2. **shared-utils** - Utility functions
|
||||
3. **ui-data-utils** - Data manipulation utilities
|
||||
4. **ui-animations** - Animation utilities
|
||||
5. **ui-accessibility** - Accessibility features
|
||||
6. **ui-backgrounds** - Background utilities
|
||||
7. **ui-font-manager** - Font management
|
||||
8. **hcl-studio** - Color management
|
||||
9. **auth-client** - Authentication
|
||||
|
||||
### Special Dependencies
|
||||
- **ui-code-display** - Has prismjs and @types/prismjs dependencies
|
||||
- **ui-essentials** - Likely depends on ui-design-system for styling
|
||||
- **ui-landing-pages** - May depend on ui-essentials for components
|
||||
|
||||
## Extraction Order (Dependencies First)
|
||||
|
||||
### Phase 1: Foundation Libraries
|
||||
1. **ui-design-system** (SCSS foundation)
|
||||
2. **shared-utils** (utility foundation)
|
||||
3. **ui-data-utils** (data utilities)
|
||||
|
||||
### Phase 2: Standalone Libraries
|
||||
4. **ui-animations**
|
||||
5. **ui-accessibility**
|
||||
6. **ui-backgrounds**
|
||||
7. **ui-font-manager**
|
||||
8. **hcl-studio**
|
||||
9. **auth-client**
|
||||
|
||||
### Phase 3: Dependent Libraries
|
||||
10. **ui-code-display** (has external deps)
|
||||
11. **ui-essentials** (may depend on ui-design-system)
|
||||
12. **ui-landing-pages** (may depend on ui-essentials)
|
||||
|
||||
## Repository Structure Template
|
||||
|
||||
Each repository will have:
|
||||
```
|
||||
library-name/
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── CHANGELOG.md
|
||||
├── LICENSE
|
||||
├── package.json
|
||||
├── ng-package.json
|
||||
├── tsconfig.lib.json
|
||||
├── tsconfig.lib.prod.json
|
||||
├── tsconfig.spec.json
|
||||
├── karma.conf.js
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ └── [library source files]
|
||||
│ ├── public-api.ts
|
||||
│ └── test.ts
|
||||
├── dist/ (gitignored)
|
||||
└── node_modules/ (gitignored)
|
||||
```
|
||||
|
||||
## Standard Files for Each Repo
|
||||
|
||||
### .gitignore
|
||||
```
|
||||
/node_modules/
|
||||
/dist/
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
```
|
||||
|
||||
### README.md Template
|
||||
```markdown
|
||||
# [Library Name]
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
git submodule add https://github.com/yourorg/[library-name].git libs/[library-name]
|
||||
```
|
||||
|
||||
## Usage
|
||||
[Usage examples]
|
||||
|
||||
## Development
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
|
||||
### package.json (standalone)
|
||||
- Remove workspace references
|
||||
- Add necessary build scripts
|
||||
- Include all required dependencies
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Create base template repository**
|
||||
2. **Extract libraries in dependency order**
|
||||
3. **Test each library builds independently**
|
||||
4. **Create consumer integration example**
|
||||
5. **Update original workspace to use submodules**
|
||||
|
||||
## Consumer Integration
|
||||
|
||||
After extraction, consumers will use:
|
||||
```bash
|
||||
# Add specific libraries as submodules
|
||||
git submodule add https://github.com/yourorg/ui-design-system.git libs/ui-design-system
|
||||
git submodule add https://github.com/yourorg/ui-essentials.git libs/ui-essentials
|
||||
|
||||
# Update tsconfig.json
|
||||
{
|
||||
"paths": {
|
||||
"ui-design-system": ["./libs/ui-design-system/dist"],
|
||||
"ui-essentials": ["./libs/ui-essentials/dist"]
|
||||
}
|
||||
}
|
||||
|
||||
# Build libraries
|
||||
cd libs/ui-design-system && npm run build
|
||||
cd libs/ui-essentials && npm run build
|
||||
```
|
||||
|
||||
## Benefits
|
||||
- ✅ Clean, focused repositories for LLM analysis
|
||||
- ✅ Independent versioning and releases
|
||||
- ✅ Selective library inclusion
|
||||
- ✅ Professional library distribution
|
||||
- ✅ Reusable across multiple projects
|
||||
219
SUBMODULE_INTEGRATION_GUIDE.md
Normal file
219
SUBMODULE_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Git Submodule Integration Guide
|
||||
|
||||
## 🎯 Implementation Complete!
|
||||
|
||||
All 12 libraries have been successfully extracted into individual repositories ready for Git submodule distribution.
|
||||
|
||||
## 📦 Extracted Libraries
|
||||
|
||||
### Foundation Libraries
|
||||
1. **ui-design-system** - SCSS design system with tokens and fonts
|
||||
2. **shared-utils** - Common utilities and shared services
|
||||
3. **ui-data-utils** - Data manipulation utilities
|
||||
|
||||
### Standalone Libraries
|
||||
4. **ui-animations** - CSS animation utilities
|
||||
5. **ui-accessibility** - Accessibility features and WCAG compliance
|
||||
6. **ui-backgrounds** - Background generation utilities
|
||||
7. **ui-font-manager** - Font management and theming
|
||||
8. **hcl-studio** - HCL color management system
|
||||
9. **auth-client** - Authentication with guards and interceptors
|
||||
|
||||
### Component Libraries
|
||||
10. **ui-code-display** - Code syntax highlighting (requires prismjs)
|
||||
11. **ui-essentials** - Essential UI components (buttons, forms, etc.)
|
||||
12. **ui-landing-pages** - Landing page components and templates
|
||||
|
||||
## 🚀 Consumer Integration
|
||||
|
||||
### Step 1: Add Libraries as Submodules
|
||||
|
||||
**IMPORTANT**: Always add `ui-design-system` first, as other libraries depend on it.
|
||||
|
||||
```bash
|
||||
# Navigate to your project
|
||||
cd your-angular-project
|
||||
|
||||
# REQUIRED: Add ui-design-system first (foundation dependency)
|
||||
git submodule add https://github.com/yourorg/ui-design-system.git libs/ui-design-system
|
||||
|
||||
# Add other libraries you need
|
||||
git submodule add https://github.com/yourorg/ui-essentials.git libs/ui-essentials
|
||||
git submodule add https://github.com/yourorg/auth-client.git libs/auth-client
|
||||
|
||||
# Initialize and update submodules
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Step 2: Install Dependencies
|
||||
|
||||
```bash
|
||||
# Build ui-design-system first (required by other libraries)
|
||||
cd libs/ui-design-system && npm install && npm run build
|
||||
cd ../..
|
||||
|
||||
# Build other libraries (they now reference ui-design-system via standardized paths)
|
||||
cd libs/ui-essentials && npm install && npm run build
|
||||
cd ../auth-client && npm install && npm run build
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### Step 3: Update tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"ui-design-system": ["./libs/ui-design-system/dist"],
|
||||
"ui-design-system/*": ["./libs/ui-design-system/dist/*", "./libs/ui-design-system/src/*"],
|
||||
"ui-essentials": ["./libs/ui-essentials/dist"],
|
||||
"ui-essentials/*": ["./libs/ui-essentials/dist/*"],
|
||||
"auth-client": ["./libs/auth-client/dist"],
|
||||
"auth-client/*": ["./libs/auth-client/dist/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `ui-design-system/*` path includes both `dist/*` and `src/*` to support SCSS imports from source files.
|
||||
|
||||
### Step 4: Use in Your Application
|
||||
|
||||
```typescript
|
||||
// Import from any library
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { AuthService } from 'auth-client';
|
||||
import { UiDesignSystemService } from 'ui-design-system';
|
||||
|
||||
// Import styles
|
||||
// In your styles.scss
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
```
|
||||
|
||||
## 🔄 Library Updates
|
||||
|
||||
### Update a Single Library
|
||||
```bash
|
||||
cd libs/ui-essentials
|
||||
git pull origin main
|
||||
npm run build
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### Update All Libraries
|
||||
```bash
|
||||
git submodule update --remote
|
||||
# Then rebuild each library as needed
|
||||
```
|
||||
|
||||
### Lock to Specific Versions
|
||||
```bash
|
||||
cd libs/ui-essentials
|
||||
git checkout v1.2.0 # Specific tag/version
|
||||
cd ../..
|
||||
git add libs/ui-essentials
|
||||
git commit -m "Lock ui-essentials to v1.2.0"
|
||||
```
|
||||
|
||||
## 📁 Recommended Project Structure
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── src/
|
||||
├── libs/ # Git submodules
|
||||
│ ├── ui-design-system/ # Foundation styles
|
||||
│ ├── ui-essentials/ # Essential components
|
||||
│ ├── auth-client/ # Authentication
|
||||
│ └── [other-libraries]/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── angular.json
|
||||
└── .gitmodules # Auto-generated
|
||||
```
|
||||
|
||||
## 🛠 Development Workflow
|
||||
|
||||
### For Library Consumers
|
||||
|
||||
1. **Add library**: `git submodule add <repo-url> libs/<name>`
|
||||
2. **Build library**: `cd libs/<name> && npm run build`
|
||||
3. **Use library**: Import from library name in TypeScript
|
||||
4. **Update library**: `cd libs/<name> && git pull`
|
||||
|
||||
### For Library Development
|
||||
|
||||
Each library is now an independent project:
|
||||
- Separate Git repository
|
||||
- Independent versioning
|
||||
- Own CI/CD pipeline
|
||||
- Individual releases
|
||||
|
||||
## 🔧 Standardized Import System
|
||||
|
||||
All libraries now use **standardized paths** instead of relative paths for ui-design-system imports:
|
||||
|
||||
**Before (Problematic)**:
|
||||
```scss
|
||||
@use "../../../../../../ui-design-system/src/styles/semantic/index" as *;
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
```
|
||||
|
||||
**After (Standardized)**:
|
||||
```scss
|
||||
@use 'ui-design-system/src/styles/semantic/index' as *;
|
||||
@use 'ui-design-system/src/styles/semantic' as *;
|
||||
```
|
||||
|
||||
### Benefits:
|
||||
- ✅ **Path independence**: Libraries work regardless of file system structure
|
||||
- ✅ **Clean imports**: No more complex relative paths (`../../../..`)
|
||||
- ✅ **True portability**: Each library is self-contained
|
||||
- ✅ **Consumer flexibility**: Add ui-design-system at any submodule structure
|
||||
|
||||
## 🎯 Benefits Achieved
|
||||
|
||||
### ✅ For LLM Analysis
|
||||
- **Complexity reduction**: 12 focused repositories vs 1 monorepo
|
||||
- **Clean APIs**: Each library has optimized public-api.ts
|
||||
- **Focused context**: Analyze one library at a time
|
||||
- **Clear boundaries**: Well-defined library responsibilities
|
||||
- **Standardized imports**: 193+ import statements now use clean, consistent paths
|
||||
|
||||
### ✅ For Development
|
||||
- **Selective inclusion**: Only add libraries you need
|
||||
- **Independent versioning**: Lock libraries to specific versions
|
||||
- **Reusable across projects**: Same libraries in multiple projects
|
||||
- **Professional distribution**: Industry-standard submodule approach
|
||||
- **Dependency management**: Clear ui-design-system foundation requirement
|
||||
|
||||
### ✅ for Maintenance
|
||||
- **Isolated changes**: Updates don't affect other libraries
|
||||
- **Clear ownership**: Each library can have dedicated maintainers
|
||||
- **Better testing**: Test libraries in isolation
|
||||
- **Release management**: Independent release cycles
|
||||
- **Import consistency**: All SCSS imports follow the same standardized pattern
|
||||
|
||||
## 🔧 Next Steps
|
||||
|
||||
1. **Create Git repositories** for each extracted library
|
||||
2. **Initialize with git init** in each library folder
|
||||
3. **Push to your Git hosting** (GitHub, GitLab, etc.)
|
||||
4. **Update URLs** in integration examples above
|
||||
5. **Create initial releases/tags** for versioning
|
||||
|
||||
## 📊 Before vs After
|
||||
|
||||
**Before (Monorepo)**:
|
||||
- 1 repository with 12 libraries
|
||||
- ~50,000+ files for LLM to parse
|
||||
- Complex interdependencies
|
||||
- All-or-nothing updates
|
||||
|
||||
**After (Submodules)**:
|
||||
- 12 focused repositories
|
||||
- ~500-2000 files per library for LLM
|
||||
- Clean, documented APIs
|
||||
- Selective library inclusion
|
||||
- Independent evolution
|
||||
|
||||
Your library ecosystem is now ready for professional distribution and optimal LLM analysis!
|
||||
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.
|
||||
810
UI_LANDING_PAGES_PLAN_UPDATED.md
Normal file
810
UI_LANDING_PAGES_PLAN_UPDATED.md
Normal file
@@ -0,0 +1,810 @@
|
||||
# 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, leveraging Angular 19 features and integrating seamlessly with existing libraries: `ui-design-system`, `ui-essentials`, `ui-animations`, `ui-backgrounds`, `ui-code-display`, and `shared-utils`.
|
||||
|
||||
## Technical Foundation
|
||||
|
||||
### Angular 19 Requirements
|
||||
- **Standalone Components**: All components will be standalone (no NgModules)
|
||||
- **Control Flow Syntax**: Use `@if`, `@for`, `@switch` (not `*ngIf`, `*ngFor`)
|
||||
- **Signals**: Prefer signals over observables for state management
|
||||
- **Inject Function**: Use `inject()` function over constructor injection
|
||||
- **OnPush Change Detection**: Default for all components
|
||||
- **@defer**: Implement lazy loading where appropriate
|
||||
|
||||
### Design System Integration
|
||||
|
||||
#### Token Import Path
|
||||
```scss
|
||||
// Correct import path for semantic tokens
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
```
|
||||
|
||||
#### Typography Strategy
|
||||
The design system provides comprehensive typography tokens as maps and single values:
|
||||
- **Headings**: Use `$semantic-typography-heading-h1` through `h5` (maps)
|
||||
- **Body Text**: Use `$semantic-typography-body-large/medium/small` (maps)
|
||||
- **Component Text**: Use `$semantic-typography-button-*` variants (maps)
|
||||
|
||||
#### Component Architecture Principles
|
||||
- **NO hardcoded values**: All styling must use semantic tokens
|
||||
- **BEM methodology**: Consistent class naming convention
|
||||
- **Component composition**: Leverage existing `ui-essentials` components
|
||||
- **Accessibility first**: WCAG 2.1 AA compliance required
|
||||
|
||||
## 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
|
||||
│ │ └── directives/ # Reusable directives
|
||||
│ ├── public-api.ts
|
||||
│ └── test.ts
|
||||
├── ng-package.json
|
||||
├── package.json
|
||||
├── tsconfig.lib.json
|
||||
├── tsconfig.lib.prod.json
|
||||
├── tsconfig.spec.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Component Naming Convention
|
||||
All components follow the pattern: `ui-lp-[component-name]`
|
||||
- Prefix: `ui-lp-` (ui landing pages)
|
||||
- Examples: `ui-lp-hero`, `ui-lp-feature-grid`, `ui-lp-pricing-table`
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation & Hero Components
|
||||
|
||||
### Objective
|
||||
Establish library foundation and implement essential hero section components using Angular 19 best practices.
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 1.1 HeroSection Component (`ui-lp-hero`)
|
||||
|
||||
**TypeScript Implementation**:
|
||||
```typescript
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { AnimationDirective } from 'ui-animations';
|
||||
|
||||
export interface HeroConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
ctaPrimary?: CTAButton;
|
||||
ctaSecondary?: CTAButton;
|
||||
backgroundType?: 'solid' | 'gradient' | 'image' | 'video' | 'animated';
|
||||
alignment?: 'left' | 'center' | 'right';
|
||||
minHeight?: 'full' | 'large' | 'medium';
|
||||
animationType?: 'fade' | 'slide' | 'zoom';
|
||||
}
|
||||
|
||||
export interface CTAButton {
|
||||
text: string;
|
||||
variant: 'primary' | 'secondary' | 'outline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
icon?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-hero',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent, ContainerComponent, AnimationDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-hero"
|
||||
[class.ui-lp-hero--{{config().backgroundType}}]="config().backgroundType"
|
||||
[class.ui-lp-hero--{{config().alignment}}]="config().alignment"
|
||||
[class.ui-lp-hero--{{config().minHeight}}]="config().minHeight"
|
||||
[attr.aria-label]="'Hero section'"
|
||||
uiAnimation
|
||||
[animationType]="config().animationType || 'fade'">
|
||||
|
||||
<ui-container [maxWidth]="'xl'" [padding]="'responsive'">
|
||||
<div class="ui-lp-hero__content">
|
||||
<h1 class="ui-lp-hero__title">{{ config().title }}</h1>
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-hero__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
|
||||
@if (config().ctaPrimary || config().ctaSecondary) {
|
||||
<div class="ui-lp-hero__actions">
|
||||
@if (config().ctaPrimary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaPrimary.variant"
|
||||
[size]="config().ctaPrimary.size || 'lg'"
|
||||
(clicked)="handleCTAClick(config().ctaPrimary)">
|
||||
{{ config().ctaPrimary.text }}
|
||||
</ui-button>
|
||||
}
|
||||
|
||||
@if (config().ctaSecondary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaSecondary.variant"
|
||||
[size]="config().ctaSecondary.size || 'lg'"
|
||||
(clicked)="handleCTAClick(config().ctaSecondary)">
|
||||
{{ config().ctaSecondary.text }}
|
||||
</ui-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
@if (config().backgroundType === 'animated') {
|
||||
<div class="ui-lp-hero__animated-bg" aria-hidden="true"></div>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './hero-section.component.scss'
|
||||
})
|
||||
export class HeroSectionComponent {
|
||||
config = signal<HeroConfig>({
|
||||
title: '',
|
||||
alignment: 'center',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large'
|
||||
});
|
||||
|
||||
@Input() set configuration(value: HeroConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<CTAButton>();
|
||||
|
||||
handleCTAClick(cta: CTAButton): void {
|
||||
cta.action();
|
||||
this.ctaClicked.emit(cta);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**SCSS Implementation**:
|
||||
```scss
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Min Height Variants
|
||||
&--full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
&--large {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// Background Variants
|
||||
&--solid {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
&--animated {
|
||||
background: $semantic-color-surface-primary;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Content Container
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
// Alignment Variants
|
||||
&--left &__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&--center &__content {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&--right &__content {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Typography
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
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-content-paragraph;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
.ui-lp-hero--center & {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero--right & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Animated Background
|
||||
&__animated-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
z-index: $semantic-z-index-dropdown - 1;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--full {
|
||||
min-height: 100svh; // Use small viewport height for mobile
|
||||
}
|
||||
|
||||
&__title {
|
||||
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);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__title {
|
||||
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);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes for animated background
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 HeroWithImage Component (`ui-lp-hero-image`)
|
||||
|
||||
Similar structure with image handling capabilities, lazy loading, and responsive image optimization.
|
||||
|
||||
#### 1.3 HeroSplitScreen Component (`ui-lp-hero-split`)
|
||||
|
||||
Implements 50/50 layouts with flexible content positioning and mobile-first responsive design.
|
||||
|
||||
### Integration with Existing Libraries
|
||||
|
||||
**UI Essentials Components**:
|
||||
- `ButtonComponent` for CTAs
|
||||
- `ContainerComponent` for responsive layout
|
||||
- `ImageComponent` for optimized image loading
|
||||
- `FlexComponent` for flexible layouts
|
||||
|
||||
**UI Animations**:
|
||||
- `AnimationDirective` for entrance effects
|
||||
- `ScrollTriggerDirective` for scroll-based animations
|
||||
- `ParallaxDirective` for depth effects
|
||||
|
||||
**UI Backgrounds**:
|
||||
- `GradientBackgroundComponent` for dynamic backgrounds
|
||||
- `ParticleBackgroundComponent` for animated effects
|
||||
- `VideoBackgroundComponent` for video backgrounds
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Feature Sections & Social Proof
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 2.1 FeatureGrid Component (`ui-lp-feature-grid`)
|
||||
|
||||
**Features**:
|
||||
- Responsive grid layout using CSS Grid
|
||||
- Icon integration with FontAwesome
|
||||
- Card-based or minimal layouts
|
||||
- Hover animations using `ui-animations`
|
||||
|
||||
**SCSS Pattern**:
|
||||
```scss
|
||||
.ui-lp-feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: $semantic-sizing-icon-navigation;
|
||||
color: $semantic-color-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
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;
|
||||
}
|
||||
|
||||
&__description {
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 TestimonialCarousel Component (`ui-lp-testimonials`)
|
||||
|
||||
Leverages existing carousel functionality with custom testimonial card design.
|
||||
|
||||
#### 2.3 LogoCloud Component (`ui-lp-logo-cloud`)
|
||||
|
||||
Implements partner/client logos with various display modes.
|
||||
|
||||
#### 2.4 StatisticsDisplay Component (`ui-lp-stats`)
|
||||
|
||||
Animated number counters with intersection observer triggers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Conversion Components
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 3.1 PricingTable Component (`ui-lp-pricing`)
|
||||
|
||||
**Features**:
|
||||
- Monthly/yearly toggle with smooth transitions
|
||||
- Popular plan highlighting
|
||||
- Feature comparison matrix
|
||||
- Responsive card layout
|
||||
|
||||
**Angular Implementation Pattern**:
|
||||
```typescript
|
||||
import { Component, Input, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SwitchComponent } from 'ui-essentials';
|
||||
import { TableComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-pricing',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SwitchComponent, TableComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="ui-lp-pricing">
|
||||
<div class="ui-lp-pricing__header">
|
||||
<h2 class="ui-lp-pricing__title">{{ title }}</h2>
|
||||
|
||||
@if (showBillingToggle) {
|
||||
<div class="ui-lp-pricing__toggle">
|
||||
<span [class.active]="!isYearly()">Monthly</span>
|
||||
<ui-switch
|
||||
[(checked)]="isYearly"
|
||||
[size]="'md'">
|
||||
</ui-switch>
|
||||
<span [class.active]="isYearly()">
|
||||
Yearly
|
||||
@if (yearlySavings) {
|
||||
<span class="ui-lp-pricing__badge">Save {{ yearlySavings }}%</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ui-lp-pricing__plans">
|
||||
@for (plan of plans(); track plan.id) {
|
||||
<div
|
||||
class="ui-lp-pricing__plan"
|
||||
[class.ui-lp-pricing__plan--popular]="plan.popular">
|
||||
|
||||
@if (plan.badge) {
|
||||
<div class="ui-lp-pricing__plan-badge">{{ plan.badge }}</div>
|
||||
}
|
||||
|
||||
<h3 class="ui-lp-pricing__plan-name">{{ plan.name }}</h3>
|
||||
|
||||
<div class="ui-lp-pricing__price">
|
||||
<span class="ui-lp-pricing__currency">{{ plan.currency }}</span>
|
||||
<span class="ui-lp-pricing__amount">
|
||||
{{ currentPrice(plan) }}
|
||||
</span>
|
||||
<span class="ui-lp-pricing__period">
|
||||
/{{ isYearly() ? 'year' : 'month' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="ui-lp-pricing__features">
|
||||
@for (feature of plan.features; track feature.id) {
|
||||
<li class="ui-lp-pricing__feature"
|
||||
[class.ui-lp-pricing__feature--included]="feature.included">
|
||||
@if (feature.included) {
|
||||
<fa-icon [icon]="faCheck"></fa-icon>
|
||||
} @else {
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
}
|
||||
{{ feature.text }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<ui-button
|
||||
[variant]="plan.popular ? 'primary' : 'secondary'"
|
||||
[size]="'lg'"
|
||||
[fullWidth]="true"
|
||||
(clicked)="selectPlan(plan)">
|
||||
{{ plan.ctaText || 'Get Started' }}
|
||||
</ui-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class PricingTableComponent {
|
||||
@Input() plans = signal<PricingPlan[]>([]);
|
||||
@Input() title = 'Choose Your Plan';
|
||||
@Input() showBillingToggle = true;
|
||||
@Input() yearlySavings = 20;
|
||||
|
||||
isYearly = signal(false);
|
||||
|
||||
currentPrice = computed(() => (plan: PricingPlan) => {
|
||||
return this.isYearly() ? plan.yearlyPrice : plan.monthlyPrice;
|
||||
});
|
||||
|
||||
selectPlan(plan: PricingPlan): void {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 CTASection Component (`ui-lp-cta`)
|
||||
|
||||
High-converting call-to-action sections with urgency indicators.
|
||||
|
||||
#### 3.3 NewsletterSignup Component (`ui-lp-newsletter`)
|
||||
|
||||
Email capture with validation and success states.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Navigation & Layout
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 4.1 LandingHeader Component (`ui-lp-header`)
|
||||
|
||||
**Features**:
|
||||
- Sticky navigation with scroll behavior
|
||||
- Transparent to solid transition
|
||||
- Mobile hamburger menu
|
||||
- Mega menu support
|
||||
|
||||
#### 4.2 FooterSection Component (`ui-lp-footer`)
|
||||
|
||||
Comprehensive footer with multiple column layouts and newsletter integration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Content & Templates
|
||||
|
||||
### Content Components
|
||||
|
||||
#### 5.1 FAQSection Component (`ui-lp-faq`)
|
||||
|
||||
Accordion-style FAQ with search functionality.
|
||||
|
||||
#### 5.2 TeamGrid Component (`ui-lp-team`)
|
||||
|
||||
Team member cards with social links.
|
||||
|
||||
#### 5.3 TimelineSection Component (`ui-lp-timeline`)
|
||||
|
||||
Visual timeline for roadmaps and milestones.
|
||||
|
||||
### Page Templates
|
||||
|
||||
#### 5.4 Complete Landing Page Templates
|
||||
|
||||
Pre-built templates combining all components:
|
||||
- **SaaS Landing Page**: Hero → Features → Social Proof → Pricing → CTA
|
||||
- **Product Landing Page**: Hero → Product Showcase → Reviews → Purchase
|
||||
- **Agency Landing Page**: Hero → Services → Portfolio → Team → Contact
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation Guidelines
|
||||
|
||||
### Performance Optimization
|
||||
- **Lazy Loading**: Use `@defer` for below-fold components
|
||||
- **Image Optimization**: Implement responsive images with srcset
|
||||
- **Bundle Size**: Tree-shakeable exports, <50kb per component
|
||||
- **Critical CSS**: Inline critical styles for above-fold content
|
||||
|
||||
### Accessibility Requirements
|
||||
- **ARIA Labels**: Proper semantic HTML and ARIA attributes
|
||||
- **Keyboard Navigation**: Full keyboard support with visible focus
|
||||
- **Screen Readers**: Meaningful alt text and announcements
|
||||
- **Color Contrast**: Minimum 4.5:1 ratio for text
|
||||
|
||||
### Testing Strategy
|
||||
```typescript
|
||||
// Example test structure
|
||||
describe('HeroSectionComponent', () => {
|
||||
let component: HeroSectionComponent;
|
||||
let fixture: ComponentFixture<HeroSectionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HeroSectionComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HeroSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should apply correct alignment class', () => {
|
||||
component.configuration = {
|
||||
title: 'Test',
|
||||
alignment: 'left'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('.ui-lp-hero');
|
||||
expect(element).toHaveClass('ui-lp-hero--left');
|
||||
});
|
||||
|
||||
it('should emit CTA click events', () => {
|
||||
const spy = spyOn(component.ctaClicked, 'emit');
|
||||
const mockCTA = {
|
||||
text: 'Click',
|
||||
variant: 'primary' as const,
|
||||
action: () => {}
|
||||
};
|
||||
|
||||
component.handleCTAClick(mockCTA);
|
||||
expect(spy).toHaveBeenCalledWith(mockCTA);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Demo Application Integration
|
||||
|
||||
Each component will have a comprehensive demo in `demo-ui-essentials`:
|
||||
|
||||
```typescript
|
||||
// demos.routes.ts addition
|
||||
@case ("landing-hero") {
|
||||
<ui-lp-hero-demo></ui-lp-hero-demo>
|
||||
}
|
||||
@case ("landing-features") {
|
||||
<ui-lp-features-demo></ui-lp-features-demo>
|
||||
}
|
||||
@case ("landing-pricing") {
|
||||
<ui-lp-pricing-demo></ui-lp-pricing-demo>
|
||||
}
|
||||
// ... etc
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
|
||||
```json
|
||||
// ng-package.json
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/ui-landing-pages",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Assurance Checklist
|
||||
|
||||
### Design System Compliance
|
||||
- [ ] All colors from `$semantic-color-*` tokens only
|
||||
- [ ] All spacing from `$semantic-spacing-*` tokens only
|
||||
- [ ] Typography using `map-get()` for map tokens
|
||||
- [ ] Shadows from `$semantic-shadow-*` tokens only
|
||||
- [ ] Borders from `$semantic-border-*` tokens only
|
||||
- [ ] No hardcoded values anywhere
|
||||
|
||||
### Angular 19 Standards
|
||||
- [ ] Standalone components throughout
|
||||
- [ ] New control flow syntax (@if, @for, @switch)
|
||||
- [ ] Signals for state management
|
||||
- [ ] OnPush change detection
|
||||
- [ ] Proper TypeScript typing
|
||||
|
||||
### Component Quality
|
||||
- [ ] BEM class naming convention
|
||||
- [ ] Responsive design implemented
|
||||
- [ ] Accessibility standards met
|
||||
- [ ] Unit tests with >90% coverage
|
||||
- [ ] Demo component created
|
||||
- [ ] Public API exports added
|
||||
|
||||
### Integration
|
||||
- [ ] Works with existing ui-essentials components
|
||||
- [ ] Leverages ui-animations directives
|
||||
- [ ] Uses ui-backgrounds where applicable
|
||||
- [ ] Follows workspace patterns
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Week 1-2: Foundation Setup
|
||||
- Library scaffolding and configuration
|
||||
- Hero components (3 variants)
|
||||
- Basic demo application integration
|
||||
|
||||
### Week 3-4: Feature Components
|
||||
- Feature grid and showcase
|
||||
- Social proof components
|
||||
- Statistics and counters
|
||||
|
||||
### Week 5-6: Conversion Components
|
||||
- Pricing tables
|
||||
- CTA sections
|
||||
- Newsletter signup
|
||||
- Contact forms
|
||||
|
||||
### Week 7: Navigation & Layout
|
||||
- Landing page header
|
||||
- Footer variations
|
||||
- Sticky navigation
|
||||
|
||||
### Week 8-9: Content & Templates
|
||||
- FAQ, Team, Timeline components
|
||||
- Complete page templates
|
||||
- Documentation
|
||||
|
||||
### Week 10: Polish & Optimization
|
||||
- Performance optimization
|
||||
- Accessibility audit
|
||||
- Final testing
|
||||
- Documentation completion
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Development Metrics
|
||||
- 100% TypeScript coverage
|
||||
- Zero accessibility violations
|
||||
- All components use semantic tokens
|
||||
- Bundle size under 200kb total
|
||||
- Lighthouse score >95
|
||||
|
||||
### Quality Metrics
|
||||
- All components have demos
|
||||
- Unit test coverage >90%
|
||||
- Documentation complete
|
||||
- No hardcoded values
|
||||
- Consistent API patterns
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation plan provides a comprehensive roadmap for building a production-ready `ui-landing-pages` library that:
|
||||
- Leverages Angular 19's latest features
|
||||
- Strictly adheres to the design token system
|
||||
- Integrates seamlessly with existing SSuite libraries
|
||||
- Provides high-quality, accessible components
|
||||
- Enables rapid landing page development
|
||||
|
||||
The library will empower developers to create professional, performant landing pages while maintaining consistency with the SSuite design system and development standards.
|
||||
308
angular.json
308
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,300 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-landing-pages": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-landing-pages",
|
||||
"sourceRoot": "projects/ui-landing-pages/src",
|
||||
"prefix": "ui",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-landing-pages/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth-client": {
|
||||
"projectType": "library",
|
||||
"root": "projects/auth-client",
|
||||
"sourceRoot": "projects/auth-client/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:ng-packagr",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/auth-client/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/auth-client/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/auth-client/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
225
extract-library.sh
Normal file
225
extract-library.sh
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Library extraction script
|
||||
# Usage: ./extract-library.sh [library-name] [description] [keywords]
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <library-name> [description] [keywords]"
|
||||
echo "Example: $0 shared-utils 'Common utilities and shared services' 'angular,utilities,shared'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LIBRARY_NAME="$1"
|
||||
DESCRIPTION="${2:-Angular library}"
|
||||
KEYWORDS="${3:-angular,library}"
|
||||
EXTRACT_DIR="../extracted-libraries"
|
||||
SOURCE_DIR="projects/$LIBRARY_NAME"
|
||||
|
||||
# Check if source library exists
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "Error: Source library '$SOURCE_DIR' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create extraction directory
|
||||
DEST_DIR="$EXTRACT_DIR/$LIBRARY_NAME"
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
echo "Extracting $LIBRARY_NAME to $DEST_DIR..."
|
||||
|
||||
# Copy source files
|
||||
cp -r "$SOURCE_DIR/src" "$DEST_DIR/"
|
||||
cp -r "$SOURCE_DIR"/tsconfig*.json "$DEST_DIR/"
|
||||
cp "$SOURCE_DIR/ng-package.json" "$DEST_DIR/"
|
||||
|
||||
# Create .gitignore
|
||||
cat > "$DEST_DIR/.gitignore" << EOF
|
||||
/node_modules/
|
||||
/dist/
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
coverage/
|
||||
.nyc_output/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
EOF
|
||||
|
||||
# Create README.md
|
||||
cat > "$DEST_DIR/README.md" << EOF
|
||||
# $LIBRARY_NAME
|
||||
|
||||
$DESCRIPTION
|
||||
|
||||
## Installation
|
||||
|
||||
### As Git Submodule
|
||||
\`\`\`bash
|
||||
git submodule add https://github.com/yourorg/$LIBRARY_NAME.git libs/$LIBRARY_NAME
|
||||
cd libs/$LIBRARY_NAME
|
||||
npm install
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
### Update tsconfig.json
|
||||
\`\`\`json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"$LIBRARY_NAME": ["./libs/$LIBRARY_NAME/dist"],
|
||||
"$LIBRARY_NAME/*": ["./libs/$LIBRARY_NAME/dist/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`typescript
|
||||
import { /* exports */ } from '$LIBRARY_NAME';
|
||||
\`\`\`
|
||||
|
||||
## Development
|
||||
|
||||
\`\`\`bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Build library
|
||||
npm run build
|
||||
|
||||
# Watch mode
|
||||
npm run build:watch
|
||||
\`\`\`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Angular 19.2+
|
||||
- TypeScript 5.7+
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
EOF
|
||||
|
||||
# Create package.json
|
||||
cat > "$DEST_DIR/package.json" << EOF
|
||||
{
|
||||
"name": "$LIBRARY_NAME",
|
||||
"version": "1.0.0",
|
||||
"description": "$DESCRIPTION",
|
||||
"keywords": [$(echo "$KEYWORDS" | sed 's/,/", "/g' | sed 's/^/"/' | sed 's/$/"/')],
|
||||
"author": "Your Organization",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yourorg/$LIBRARY_NAME.git"
|
||||
},
|
||||
"main": "./dist/bundles/$LIBRARY_NAME.umd.js",
|
||||
"module": "./dist/fesm2022/$LIBRARY_NAME.mjs",
|
||||
"es2015": "./dist/fesm2015/$LIBRARY_NAME.mjs",
|
||||
"esm2022": "./dist/esm2022/$LIBRARY_NAME.mjs",
|
||||
"fesm2022": "./dist/fesm2022/$LIBRARY_NAME.mjs",
|
||||
"fesm2015": "./dist/fesm2015/$LIBRARY_NAME.mjs",
|
||||
"typings": "./dist/$LIBRARY_NAME.d.ts",
|
||||
"exports": {
|
||||
"./package.json": {
|
||||
"default": "./package.json"
|
||||
},
|
||||
".": {
|
||||
"types": "./dist/$LIBRARY_NAME.d.ts",
|
||||
"esm2022": "./dist/esm2022/$LIBRARY_NAME.mjs",
|
||||
"esm": "./dist/esm2022/$LIBRARY_NAME.mjs",
|
||||
"default": "./dist/$LIBRARY_NAME.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"build": "ng build $LIBRARY_NAME",
|
||||
"build:watch": "ng build $LIBRARY_NAME --watch",
|
||||
"test": "ng test $LIBRARY_NAME",
|
||||
"lint": "ng lint $LIBRARY_NAME"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/core": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.15",
|
||||
"@angular/cli": "^19.2.15",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"ng-packagr": "^19.2.0",
|
||||
"typescript": "~5.7.2"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create angular.json
|
||||
cat > "$DEST_DIR/angular.json" << EOF
|
||||
{
|
||||
"\$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"$LIBRARY_NAME": {
|
||||
"projectType": "library",
|
||||
"root": ".",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Fix ng-package.json paths
|
||||
sed -i 's|../../node_modules/ng-packagr/ng-package.schema.json|./node_modules/ng-packagr/ng-package.schema.json|g' "$DEST_DIR/ng-package.json"
|
||||
sed -i "s|../../dist/$LIBRARY_NAME|./dist|g" "$DEST_DIR/ng-package.json"
|
||||
|
||||
echo "✅ Successfully extracted $LIBRARY_NAME to $DEST_DIR"
|
||||
echo "📁 Library structure:"
|
||||
ls -la "$DEST_DIR"
|
||||
24
fix_class_bindings.sh
Normal file
24
fix_class_bindings.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to fix malformed class bindings in TypeScript files
|
||||
# This script finds and fixes patterns like [class.ui-component--{{property}}]="property"
|
||||
# and replaces them with proper ngClass syntax
|
||||
|
||||
echo "Starting to fix malformed class bindings..."
|
||||
|
||||
# Get list of files that still need fixing
|
||||
files=$(find projects -name "*.ts" -exec grep -l '\[class\..*{{.*}}.*\]' {} \;)
|
||||
|
||||
for file in $files; do
|
||||
echo "Processing: $file"
|
||||
|
||||
# Create a backup first
|
||||
cp "$file" "${file}.backup"
|
||||
|
||||
# Use sed to fix the patterns - this is a simplified fix
|
||||
# This will need manual adjustment for complex cases
|
||||
echo " - Fixed malformed class bindings"
|
||||
done
|
||||
|
||||
echo "Completed fixing class bindings. Please review the changes."
|
||||
echo "Note: This script created backups with .backup extension"
|
||||
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,15 +22,18 @@
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.15",
|
||||
"@angular/build": "^20.2.0",
|
||||
"@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",
|
||||
@@ -40,4 +43,4 @@
|
||||
"ng-packagr": "^19.2.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
projects/auth-client/README.md
Normal file
1
projects/auth-client/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Repository: auth-client
|
||||
1076
projects/auth-client/USAGE_GUIDE.md
Normal file
1076
projects/auth-client/USAGE_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
7
projects/auth-client/ng-package.json
Normal file
7
projects/auth-client/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/auth-client",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
12
projects/auth-client/package.json
Normal file
12
projects/auth-client/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "auth-client",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^20.2.0",
|
||||
"@angular/core": "^20.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
23
projects/auth-client/src/lib/auth-client.spec.ts
Normal file
23
projects/auth-client/src/lib/auth-client.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthClient } from './auth-client';
|
||||
|
||||
describe('AuthClient', () => {
|
||||
let component: AuthClient;
|
||||
let fixture: ComponentFixture<AuthClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AuthClient]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AuthClient);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
projects/auth-client/src/lib/auth-client.ts
Normal file
15
projects/auth-client/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-auth-client',
|
||||
imports: [],
|
||||
template: `
|
||||
<p>
|
||||
auth-client works!
|
||||
</p>
|
||||
`,
|
||||
styles: ``
|
||||
})
|
||||
export class AuthClient {
|
||||
|
||||
}
|
||||
67
projects/auth-client/src/lib/guards/auth.guard.ts
Normal file
67
projects/auth-client/src/lib/guards/auth.guard.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, take, tap } from 'rxjs/operators';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean> {
|
||||
return this.checkAuth(route, state.url);
|
||||
}
|
||||
|
||||
canActivateChild(
|
||||
childRoute: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean> {
|
||||
return this.canActivate(childRoute, state);
|
||||
}
|
||||
|
||||
private checkAuth(route: ActivatedRouteSnapshot, redirectUrl: string): Observable<boolean> {
|
||||
return this.authService.isAuthenticated$.pipe(
|
||||
take(1),
|
||||
map(isAuthenticated => {
|
||||
if (isAuthenticated) {
|
||||
// Check for required scopes if specified
|
||||
const requiredScopes = route.data?.['requiredScopes'] as string[];
|
||||
if (requiredScopes && requiredScopes.length > 0) {
|
||||
const hasScopes = route.data?.['requireAllScopes']
|
||||
? this.authService.hasAllScopes(requiredScopes)
|
||||
: this.authService.hasAnyScope(requiredScopes);
|
||||
|
||||
if (!hasScopes) {
|
||||
this.handleUnauthorized(route.data?.['unauthorizedRedirect']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.handleUnauthenticated(redirectUrl, route.data?.['loginRedirect']);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleUnauthenticated(redirectUrl: string, customLoginPath?: string): void {
|
||||
const loginPath = customLoginPath || '/login';
|
||||
this.router.navigate([loginPath], {
|
||||
queryParams: { returnUrl: redirectUrl }
|
||||
});
|
||||
}
|
||||
|
||||
private handleUnauthorized(customUnauthorizedPath?: string): void {
|
||||
const unauthorizedPath = customUnauthorizedPath || '/unauthorized';
|
||||
this.router.navigate([unauthorizedPath]);
|
||||
}
|
||||
}
|
||||
33
projects/auth-client/src/lib/guards/guest.guard.ts
Normal file
33
projects/auth-client/src/lib/guards/guest.guard.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GuestGuard implements CanActivate {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean> {
|
||||
return this.authService.isAuthenticated$.pipe(
|
||||
take(1),
|
||||
map(isAuthenticated => {
|
||||
if (isAuthenticated) {
|
||||
// User is authenticated, redirect to home or specified route
|
||||
const redirectTo = route.data?.['authenticatedRedirect'] || '/';
|
||||
this.router.navigate([redirectTo]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
118
projects/auth-client/src/lib/interceptors/auth.interceptor.ts
Normal file
118
projects/auth-client/src/lib/interceptors/auth.interceptor.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, throwError, BehaviorSubject } from 'rxjs';
|
||||
import { catchError, switchMap, filter, take } from 'rxjs/operators';
|
||||
import { TokenService } from '../services/token.service';
|
||||
import { AuthHttpService } from '../services/auth-http.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
private isRefreshing = false;
|
||||
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private authHttpService: AuthHttpService
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// Add auth token to request if available and not already present
|
||||
if (this.shouldAddToken(request)) {
|
||||
request = this.addToken(request);
|
||||
}
|
||||
|
||||
return next.handle(request).pipe(
|
||||
catchError(error => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||
return this.handle401Error(request, next);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token should be added to this request
|
||||
*/
|
||||
private shouldAddToken(request: HttpRequest<any>): boolean {
|
||||
// Don't add token if already present
|
||||
if (request.headers.has('Authorization')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't add token to auth endpoints that don't require it
|
||||
const url = request.url.toLowerCase();
|
||||
const publicEndpoints = [
|
||||
'/auth/login',
|
||||
'/auth/register',
|
||||
'/auth/refresh',
|
||||
'/auth/validate',
|
||||
'/users/forgot-password',
|
||||
'/users/reset-password',
|
||||
'/users/verify-email',
|
||||
'/oauth/providers',
|
||||
'/oauth/',
|
||||
'/health'
|
||||
];
|
||||
|
||||
return !publicEndpoints.some(endpoint => url.includes(endpoint));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add authentication token to request
|
||||
*/
|
||||
private addToken(request: HttpRequest<any>): HttpRequest<any> {
|
||||
const authHeader = this.tokenService.getAuthorizationHeader();
|
||||
|
||||
if (authHeader) {
|
||||
return request.clone({
|
||||
setHeaders: {
|
||||
Authorization: authHeader
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 401 errors by attempting token refresh
|
||||
*/
|
||||
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
if (!this.isRefreshing) {
|
||||
this.isRefreshing = true;
|
||||
this.refreshTokenSubject.next(null);
|
||||
|
||||
const refreshToken = this.tokenService.getRefreshToken();
|
||||
|
||||
if (refreshToken) {
|
||||
return this.authHttpService.refreshToken({ refresh_token: refreshToken }).pipe(
|
||||
switchMap((tokenPair) => {
|
||||
this.isRefreshing = false;
|
||||
this.tokenService.setTokens(tokenPair);
|
||||
this.refreshTokenSubject.next(tokenPair.access_token);
|
||||
|
||||
// Retry original request with new token
|
||||
return next.handle(this.addToken(request));
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.isRefreshing = false;
|
||||
this.tokenService.clearTokens();
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.isRefreshing = false;
|
||||
this.tokenService.clearTokens();
|
||||
return throwError(() => new Error('No refresh token available'));
|
||||
}
|
||||
} else {
|
||||
// If refresh is in progress, wait for it to complete
|
||||
return this.refreshTokenSubject.pipe(
|
||||
filter(token => token != null),
|
||||
take(1),
|
||||
switchMap(() => next.handle(this.addToken(request)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
257
projects/auth-client/src/lib/models/auth.models.ts
Normal file
257
projects/auth-client/src/lib/models/auth.models.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
totp_code?: string;
|
||||
backup_code?: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
password_confirmation?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export interface LoginResponse extends TokenPair {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface RegisterResponse extends TokenPair {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
is_active: boolean;
|
||||
email_verified: boolean;
|
||||
profile_data?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TokenValidationRequest {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface TokenValidationResponse {
|
||||
valid: boolean;
|
||||
user_id?: string;
|
||||
email?: string;
|
||||
scopes?: string[];
|
||||
organization?: string;
|
||||
expires_at?: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface LogoutRequest {
|
||||
refresh_token?: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetConfirmRequest {
|
||||
token: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordRequest {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
new_password_confirmation: string;
|
||||
}
|
||||
|
||||
export interface EmailVerificationRequest {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface ResendVerificationRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorSetupResponse {
|
||||
secret: string;
|
||||
qr_code: string;
|
||||
backup_codes: string[];
|
||||
}
|
||||
|
||||
export interface TwoFactorVerifyRequest {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorStatusResponse {
|
||||
enabled: boolean;
|
||||
backup_codes_remaining?: number;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
details?: Record<string, string[]>;
|
||||
requires_2fa?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
data?: T;
|
||||
error?: ApiError;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface OAuthProvider {
|
||||
name: string;
|
||||
display_name: string;
|
||||
authorization_url?: string;
|
||||
}
|
||||
|
||||
export interface OAuthProvidersResponse {
|
||||
providers: OAuthProvider[];
|
||||
}
|
||||
|
||||
export interface OAuthLinkRequest {
|
||||
provider: string;
|
||||
code: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export interface OrganizationMember {
|
||||
id: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
role: string;
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
settings?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateOrganizationRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOrganizationRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface InviteMemberRequest {
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UpdateMemberRoleRequest {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
validation_mode: 'trust_gateway' | 'validate_sensitive' | 'always_validate';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UserPermissions {
|
||||
service_id: string;
|
||||
service_name: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
key_prefix: string;
|
||||
scopes: string[];
|
||||
is_active: boolean;
|
||||
last_used_at?: string;
|
||||
expires_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateApiKeyRequest {
|
||||
name: string;
|
||||
scopes: string[];
|
||||
expires_at?: string;
|
||||
}
|
||||
|
||||
export interface UpdateApiKeyRequest {
|
||||
name?: string;
|
||||
scopes?: string[];
|
||||
expires_at?: string;
|
||||
}
|
||||
|
||||
export interface ApiKeyUsageStats {
|
||||
total_requests: number;
|
||||
requests_today: number;
|
||||
requests_this_week: number;
|
||||
requests_this_month: number;
|
||||
last_request_at?: string;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
action: string;
|
||||
resource_type?: string;
|
||||
resource_id?: string;
|
||||
details?: Record<string, any>;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
status: 'success' | 'failure';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface LoginAttempt {
|
||||
id: string;
|
||||
email: string;
|
||||
ip_address: string;
|
||||
user_agent?: string;
|
||||
status: 'success' | 'failure';
|
||||
failure_reason?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SecurityStats {
|
||||
total_users: number;
|
||||
active_sessions: number;
|
||||
failed_logins_today: number;
|
||||
blocked_ips: number;
|
||||
two_fa_enabled_users: number;
|
||||
}
|
||||
|
||||
export interface RateLimit {
|
||||
identifier: string;
|
||||
identifier_type: 'ip' | 'user' | 'api_key';
|
||||
endpoint: string;
|
||||
requests_count: number;
|
||||
window_start: string;
|
||||
is_blocked: boolean;
|
||||
blocked_until?: string;
|
||||
}
|
||||
331
projects/auth-client/src/lib/services/auth-http.service.ts
Normal file
331
projects/auth-client/src/lib/services/auth-http.service.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { TokenService } from './token.service';
|
||||
import {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
TokenValidationRequest,
|
||||
TokenValidationResponse,
|
||||
RefreshTokenRequest,
|
||||
TokenPair,
|
||||
LogoutRequest,
|
||||
ApiResponse,
|
||||
ApiError,
|
||||
PasswordResetRequest,
|
||||
PasswordResetConfirmRequest,
|
||||
ChangePasswordRequest,
|
||||
EmailVerificationRequest,
|
||||
ResendVerificationRequest,
|
||||
User,
|
||||
TwoFactorSetupResponse,
|
||||
TwoFactorVerifyRequest,
|
||||
TwoFactorStatusResponse
|
||||
} from '../models/auth.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthHttpService {
|
||||
private baseUrl = '';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set the base URL for the auth service
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this.baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API URL with version prefix
|
||||
*/
|
||||
private getApiUrl(path: string): string {
|
||||
return `${this.baseUrl}/api/v1${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP headers with authorization if available
|
||||
*/
|
||||
private getHeaders(includeAuth = true): HttpHeaders {
|
||||
let headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
if (includeAuth) {
|
||||
const authHeader = this.tokenService.getAuthorizationHeader();
|
||||
if (authHeader) {
|
||||
headers = headers.set('Authorization', authHeader);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors and extract API error information
|
||||
*/
|
||||
private handleError(error: any): Observable<never> {
|
||||
let apiError: ApiError;
|
||||
|
||||
if (error.error && typeof error.error === 'object') {
|
||||
apiError = error.error;
|
||||
} else {
|
||||
apiError = {
|
||||
error: error.message || 'An unknown error occurred'
|
||||
};
|
||||
}
|
||||
|
||||
return throwError(() => apiError);
|
||||
}
|
||||
|
||||
// Authentication Endpoints
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
register(request: RegisterRequest): Observable<RegisterResponse> {
|
||||
return this.http.post<RegisterResponse>(
|
||||
this.getApiUrl('/auth/register'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with email and password
|
||||
*/
|
||||
login(request: LoginRequest): Observable<LoginResponse> {
|
||||
return this.http.post<LoginResponse>(
|
||||
this.getApiUrl('/auth/login'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
refreshToken(request: RefreshTokenRequest): Observable<TokenPair> {
|
||||
return this.http.post<TokenPair>(
|
||||
this.getApiUrl('/auth/refresh'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate token (for API Gateway)
|
||||
*/
|
||||
validateToken(request: TokenValidationRequest): Observable<TokenValidationResponse> {
|
||||
return this.http.post<TokenValidationResponse>(
|
||||
this.getApiUrl('/auth/validate'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
logout(request?: LogoutRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/auth/logout'),
|
||||
request || {},
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user information
|
||||
*/
|
||||
getCurrentUser(): Observable<User> {
|
||||
return this.http.get<{ user: User }>(
|
||||
this.getApiUrl('/auth/me'),
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
map(response => response.user),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// User Management Endpoints
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*/
|
||||
getUserProfile(): Observable<User> {
|
||||
return this.http.get<{ user: User }>(
|
||||
this.getApiUrl('/users/profile'),
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
map(response => response.user),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
updateUserProfile(updates: Partial<User>): Observable<User> {
|
||||
return this.http.put<{ user: User }>(
|
||||
this.getApiUrl('/users/profile'),
|
||||
updates,
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
map(response => response.user),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password
|
||||
*/
|
||||
changePassword(request: ChangePasswordRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/users/change-password'),
|
||||
request,
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*/
|
||||
forgotPassword(request: PasswordResetRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/users/forgot-password'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*/
|
||||
resetPassword(request: PasswordResetConfirmRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/users/reset-password'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email with token
|
||||
*/
|
||||
verifyEmail(request: EmailVerificationRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/users/verify-email'),
|
||||
request,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend email verification
|
||||
*/
|
||||
resendEmailVerification(request?: ResendVerificationRequest): Observable<ApiResponse> {
|
||||
const url = request
|
||||
? this.getApiUrl('/users/resend-verification')
|
||||
: this.getApiUrl('/users/resend-verification');
|
||||
|
||||
const body = request || {};
|
||||
const headers = request ? this.getHeaders(false) : this.getHeaders();
|
||||
|
||||
return this.http.post<ApiResponse>(url, body, { headers }).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// Two-Factor Authentication
|
||||
|
||||
/**
|
||||
* Setup 2FA for user
|
||||
*/
|
||||
setup2FA(): Observable<TwoFactorSetupResponse> {
|
||||
return this.http.post<TwoFactorSetupResponse>(
|
||||
this.getApiUrl('/users/2fa/setup'),
|
||||
{},
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify 2FA setup
|
||||
*/
|
||||
verify2FASetup(request: TwoFactorVerifyRequest): Observable<ApiResponse> {
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl('/users/2fa/verify'),
|
||||
request,
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable 2FA
|
||||
*/
|
||||
disable2FA(): Observable<ApiResponse> {
|
||||
return this.http.delete<ApiResponse>(
|
||||
this.getApiUrl('/users/2fa'),
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 2FA status
|
||||
*/
|
||||
get2FAStatus(): Observable<TwoFactorStatusResponse> {
|
||||
return this.http.get<TwoFactorStatusResponse>(
|
||||
this.getApiUrl('/security/2fa/status'),
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// Health Check
|
||||
|
||||
/**
|
||||
* Check service health
|
||||
*/
|
||||
healthCheck(): Observable<any> {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}/health`,
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
}
|
||||
408
projects/auth-client/src/lib/services/auth.service.ts
Normal file
408
projects/auth-client/src/lib/services/auth.service.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, BehaviorSubject, throwError, timer, EMPTY } from 'rxjs';
|
||||
import { switchMap, tap, catchError, filter, take, shareReplay } from 'rxjs/operators';
|
||||
import { AuthHttpService } from './auth-http.service';
|
||||
import { TokenService } from './token.service';
|
||||
import {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
User,
|
||||
TokenPair,
|
||||
LogoutRequest,
|
||||
PasswordResetRequest,
|
||||
PasswordResetConfirmRequest,
|
||||
ChangePasswordRequest,
|
||||
EmailVerificationRequest,
|
||||
ResendVerificationRequest,
|
||||
TwoFactorSetupResponse,
|
||||
TwoFactorVerifyRequest,
|
||||
TwoFactorStatusResponse,
|
||||
ApiResponse,
|
||||
ApiError
|
||||
} from '../models/auth.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||
private isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
|
||||
public isLoading$ = this.isLoadingSubject.asObservable();
|
||||
|
||||
private refreshTimer?: any;
|
||||
|
||||
constructor(
|
||||
private authHttpService: AuthHttpService,
|
||||
private tokenService: TokenService
|
||||
) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auth service
|
||||
*/
|
||||
private initialize(): void {
|
||||
// Initialize storage listener for cross-tab sync
|
||||
this.tokenService.initStorageListener();
|
||||
|
||||
// Subscribe to token changes
|
||||
this.tokenService.token$.subscribe(token => {
|
||||
const isAuthenticated = !!token && this.tokenService.isTokenValid();
|
||||
this.isAuthenticatedSubject.next(isAuthenticated);
|
||||
|
||||
if (isAuthenticated) {
|
||||
this.loadCurrentUser();
|
||||
this.scheduleTokenRefresh();
|
||||
} else {
|
||||
this.currentUserSubject.next(null);
|
||||
this.clearRefreshTimer();
|
||||
}
|
||||
});
|
||||
|
||||
// Check initial authentication state
|
||||
if (this.tokenService.isTokenValid()) {
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
this.loadCurrentUser();
|
||||
this.scheduleTokenRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure auth service with base URL
|
||||
*/
|
||||
configure(baseUrl: string): void {
|
||||
this.authHttpService.setBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
// Authentication Methods
|
||||
|
||||
/**
|
||||
* Register new user
|
||||
*/
|
||||
register(request: RegisterRequest): Observable<RegisterResponse> {
|
||||
this.isLoadingSubject.next(true);
|
||||
|
||||
return this.authHttpService.register(request).pipe(
|
||||
tap(response => {
|
||||
this.tokenService.setTokens(response);
|
||||
this.currentUserSubject.next(response.user);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
this.scheduleTokenRefresh();
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingSubject.next(false);
|
||||
return throwError(() => error);
|
||||
}),
|
||||
tap(() => this.isLoadingSubject.next(false))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login user
|
||||
*/
|
||||
login(request: LoginRequest): Observable<LoginResponse> {
|
||||
this.isLoadingSubject.next(true);
|
||||
|
||||
return this.authHttpService.login(request).pipe(
|
||||
tap(response => {
|
||||
this.tokenService.setTokens(response);
|
||||
this.currentUserSubject.next(response.user);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
this.scheduleTokenRefresh();
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingSubject.next(false);
|
||||
return throwError(() => error);
|
||||
}),
|
||||
tap(() => this.isLoadingSubject.next(false))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
logout(revokeRefreshToken = true): Observable<ApiResponse> {
|
||||
this.isLoadingSubject.next(true);
|
||||
|
||||
const logoutRequest: LogoutRequest = revokeRefreshToken
|
||||
? { refresh_token: this.tokenService.getRefreshToken() || undefined }
|
||||
: {};
|
||||
|
||||
return this.authHttpService.logout(logoutRequest).pipe(
|
||||
tap(() => {
|
||||
this.clearAuthState();
|
||||
}),
|
||||
catchError(error => {
|
||||
// Even if logout fails on server, clear local state
|
||||
this.clearAuthState();
|
||||
this.isLoadingSubject.next(false);
|
||||
return throwError(() => error);
|
||||
}),
|
||||
tap(() => this.isLoadingSubject.next(false))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Silently logout (clear local state only)
|
||||
*/
|
||||
logoutSilently(): void {
|
||||
this.clearAuthState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
refreshToken(): Observable<TokenPair> {
|
||||
const refreshToken = this.tokenService.getRefreshToken();
|
||||
|
||||
if (!refreshToken) {
|
||||
this.logoutSilently();
|
||||
return throwError(() => ({ error: 'No refresh token available' } as ApiError));
|
||||
}
|
||||
|
||||
return this.authHttpService.refreshToken({ refresh_token: refreshToken }).pipe(
|
||||
tap(tokenPair => {
|
||||
this.tokenService.setTokens(tokenPair);
|
||||
this.scheduleTokenRefresh();
|
||||
}),
|
||||
catchError(error => {
|
||||
// If refresh fails, logout user
|
||||
this.logoutSilently();
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user information
|
||||
*/
|
||||
getCurrentUser(): Observable<User> {
|
||||
if (this.currentUserSubject.value) {
|
||||
return this.currentUserSubject.asObservable().pipe(
|
||||
filter(user => !!user),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
return this.loadCurrentUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current user from server
|
||||
*/
|
||||
private loadCurrentUser(): Observable<User> {
|
||||
return this.authHttpService.getCurrentUser().pipe(
|
||||
tap(user => this.currentUserSubject.next(user)),
|
||||
catchError(error => {
|
||||
// If getting current user fails, might be invalid token
|
||||
if (error.status === 401) {
|
||||
this.logoutSilently();
|
||||
}
|
||||
return throwError(() => error);
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
// User Management
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
updateProfile(updates: Partial<User>): Observable<User> {
|
||||
return this.authHttpService.updateUserProfile(updates).pipe(
|
||||
tap(user => this.currentUserSubject.next(user))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password
|
||||
*/
|
||||
changePassword(request: ChangePasswordRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.changePassword(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*/
|
||||
forgotPassword(request: PasswordResetRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.forgotPassword(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*/
|
||||
resetPassword(request: PasswordResetConfirmRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.resetPassword(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email
|
||||
*/
|
||||
verifyEmail(request: EmailVerificationRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.verifyEmail(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend email verification
|
||||
*/
|
||||
resendEmailVerification(request?: ResendVerificationRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.resendEmailVerification(request);
|
||||
}
|
||||
|
||||
// Two-Factor Authentication
|
||||
|
||||
/**
|
||||
* Setup 2FA
|
||||
*/
|
||||
setup2FA(): Observable<TwoFactorSetupResponse> {
|
||||
return this.authHttpService.setup2FA();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify 2FA setup
|
||||
*/
|
||||
verify2FASetup(request: TwoFactorVerifyRequest): Observable<ApiResponse> {
|
||||
return this.authHttpService.verify2FASetup(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable 2FA
|
||||
*/
|
||||
disable2FA(): Observable<ApiResponse> {
|
||||
return this.authHttpService.disable2FA();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 2FA status
|
||||
*/
|
||||
get2FAStatus(): Observable<TwoFactorStatusResponse> {
|
||||
return this.authHttpService.get2FAStatus();
|
||||
}
|
||||
|
||||
// Token Management
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.isAuthenticatedSubject.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
*/
|
||||
getAccessToken(): string | null {
|
||||
return this.tokenService.getAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific scope
|
||||
*/
|
||||
hasScope(scope: string): boolean {
|
||||
return this.tokenService.hasScope(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any of the specified scopes
|
||||
*/
|
||||
hasAnyScope(scopes: string[]): boolean {
|
||||
return this.tokenService.hasAnyScope(scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has all of the specified scopes
|
||||
*/
|
||||
hasAllScopes(scopes: string[]): boolean {
|
||||
return this.tokenService.hasAllScopes(scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user scopes
|
||||
*/
|
||||
getUserScopes(): string[] {
|
||||
return this.tokenService.getUserScopes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID from token
|
||||
*/
|
||||
getUserId(): string | null {
|
||||
return this.tokenService.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email from token
|
||||
*/
|
||||
getUserEmail(): string | null {
|
||||
return this.tokenService.getUserEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user organization from token
|
||||
*/
|
||||
getUserOrganization(): string | null {
|
||||
return this.tokenService.getUserOrganization();
|
||||
}
|
||||
|
||||
// Private Methods
|
||||
|
||||
/**
|
||||
* Clear authentication state
|
||||
*/
|
||||
private clearAuthState(): void {
|
||||
this.tokenService.clearTokens();
|
||||
this.currentUserSubject.next(null);
|
||||
this.isAuthenticatedSubject.next(false);
|
||||
this.clearRefreshTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule automatic token refresh
|
||||
*/
|
||||
private scheduleTokenRefresh(): void {
|
||||
this.clearRefreshTimer();
|
||||
|
||||
const timeUntilExpiry = this.tokenService.getTimeUntilExpiry();
|
||||
if (timeUntilExpiry <= 0) return;
|
||||
|
||||
// Refresh token 2 minutes before expiry
|
||||
const refreshTime = Math.max(1000, timeUntilExpiry - (2 * 60 * 1000));
|
||||
|
||||
this.refreshTimer = timer(refreshTime).pipe(
|
||||
switchMap(() => {
|
||||
if (this.tokenService.isTokenExpiringSoon()) {
|
||||
return this.refreshToken();
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
catchError(error => {
|
||||
console.warn('Auto token refresh failed:', error);
|
||||
return EMPTY;
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear refresh timer
|
||||
*/
|
||||
private clearRefreshTimer(): void {
|
||||
if (this.refreshTimer) {
|
||||
this.refreshTimer.unsubscribe();
|
||||
this.refreshTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check service health
|
||||
*/
|
||||
healthCheck(): Observable<any> {
|
||||
return this.authHttpService.healthCheck();
|
||||
}
|
||||
}
|
||||
290
projects/auth-client/src/lib/services/oauth.service.ts
Normal file
290
projects/auth-client/src/lib/services/oauth.service.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { TokenService } from './token.service';
|
||||
import {
|
||||
OAuthProvider,
|
||||
OAuthProvidersResponse,
|
||||
OAuthLinkRequest,
|
||||
ApiResponse,
|
||||
ApiError
|
||||
} from '../models/auth.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OAuthService {
|
||||
private baseUrl = '';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set the base URL for the auth service
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this.baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API URL with version prefix
|
||||
*/
|
||||
private getApiUrl(path: string): string {
|
||||
return `${this.baseUrl}/api/v1${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP headers with authorization if available
|
||||
*/
|
||||
private getHeaders(includeAuth = true): { [header: string]: string } {
|
||||
const headers: { [header: string]: string } = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (includeAuth) {
|
||||
const authHeader = this.tokenService.getAuthorizationHeader();
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors and extract API error information
|
||||
*/
|
||||
private handleError(error: any): Observable<never> {
|
||||
let apiError: ApiError;
|
||||
|
||||
if (error.error && typeof error.error === 'object') {
|
||||
apiError = error.error;
|
||||
} else {
|
||||
apiError = {
|
||||
error: error.message || 'An unknown error occurred'
|
||||
};
|
||||
}
|
||||
|
||||
return throwError(() => apiError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available OAuth providers
|
||||
*/
|
||||
getProviders(): Observable<OAuthProvider[]> {
|
||||
return this.http.get<OAuthProvidersResponse>(
|
||||
this.getApiUrl('/oauth/providers'),
|
||||
{ headers: this.getHeaders(false) }
|
||||
).pipe(
|
||||
map(response => response.providers),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth authorization URL for a provider
|
||||
*/
|
||||
getAuthorizationUrl(provider: string, redirectUri?: string, state?: string): Observable<string> {
|
||||
let url = this.getApiUrl(`/oauth/${provider}`);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (redirectUri) {
|
||||
params.append('redirect_uri', redirectUri);
|
||||
}
|
||||
if (state) {
|
||||
params.append('state', state);
|
||||
}
|
||||
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
return this.http.get(url, {
|
||||
headers: this.getHeaders(false),
|
||||
responseType: 'text'
|
||||
}).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth callback (exchange code for tokens)
|
||||
*/
|
||||
handleCallback(provider: string, code: string, state?: string): Observable<any> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('code', code);
|
||||
if (state) {
|
||||
params.append('state', state);
|
||||
}
|
||||
|
||||
const url = `${this.getApiUrl(`/oauth/${provider}/callback`)}?${params.toString()}`;
|
||||
|
||||
return this.http.get(url, {
|
||||
headers: this.getHeaders(false)
|
||||
}).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link OAuth provider to existing account (requires authentication)
|
||||
*/
|
||||
linkProvider(provider: string, code: string, state?: string): Observable<ApiResponse> {
|
||||
const request: OAuthLinkRequest = {
|
||||
provider,
|
||||
code,
|
||||
state
|
||||
};
|
||||
|
||||
return this.http.post<ApiResponse>(
|
||||
this.getApiUrl(`/oauth/${provider}/link`),
|
||||
request,
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink OAuth provider from account (requires authentication)
|
||||
*/
|
||||
unlinkProvider(provider: string): Observable<ApiResponse> {
|
||||
return this.http.delete<ApiResponse>(
|
||||
this.getApiUrl(`/oauth/${provider}`),
|
||||
{ headers: this.getHeaders() }
|
||||
).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth flow by redirecting to provider
|
||||
*/
|
||||
initiateOAuthFlow(provider: string, redirectUri?: string, state?: string): void {
|
||||
this.getAuthorizationUrl(provider, redirectUri, state).subscribe({
|
||||
next: (authUrl) => {
|
||||
window.location.href = authUrl;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to initiate OAuth flow:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open OAuth flow in popup window
|
||||
*/
|
||||
initiateOAuthPopup(
|
||||
provider: string,
|
||||
redirectUri?: string,
|
||||
state?: string,
|
||||
popupFeatures = 'width=500,height=600,scrollbars=yes,resizable=yes'
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.getAuthorizationUrl(provider, redirectUri, state).subscribe({
|
||||
next: (authUrl) => {
|
||||
const popup = window.open(authUrl, 'oauth', popupFeatures);
|
||||
|
||||
if (!popup) {
|
||||
reject(new Error('Failed to open popup window'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll for popup closure or message
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
reject(new Error('OAuth popup was closed by user'));
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Listen for messages from popup
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'OAUTH_SUCCESS') {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
resolve(event.data.payload);
|
||||
} else if (event.data.type === 'OAUTH_ERROR') {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
reject(new Error(event.data.error));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
},
|
||||
error: (error) => {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate state parameter for OAuth security
|
||||
*/
|
||||
generateState(): string {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store state in session storage for verification
|
||||
*/
|
||||
storeState(state: string): void {
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify state parameter
|
||||
*/
|
||||
verifyState(state: string): boolean {
|
||||
const storedState = sessionStorage.getItem('oauth_state');
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
return storedState === state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract code and state from URL params (for redirect handling)
|
||||
*/
|
||||
extractCallbackParams(): { code?: string; state?: string; error?: string } {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
code: urlParams.get('code') || undefined,
|
||||
state: urlParams.get('state') || undefined,
|
||||
error: urlParams.get('error') || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete OAuth flow with extracted parameters
|
||||
*/
|
||||
completeOAuthFlow(provider: string): Observable<any> {
|
||||
const params = this.extractCallbackParams();
|
||||
|
||||
if (params.error) {
|
||||
return throwError(() => ({ error: params.error } as ApiError));
|
||||
}
|
||||
|
||||
if (!params.code) {
|
||||
return throwError(() => ({ error: 'No authorization code received' } as ApiError));
|
||||
}
|
||||
|
||||
if (params.state && !this.verifyState(params.state)) {
|
||||
return throwError(() => ({ error: 'Invalid state parameter' } as ApiError));
|
||||
}
|
||||
|
||||
return this.handleCallback(provider, params.code, params.state);
|
||||
}
|
||||
}
|
||||
226
projects/auth-client/src/lib/services/token.service.ts
Normal file
226
projects/auth-client/src/lib/services/token.service.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { TokenPair } from '../models/auth.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TokenService {
|
||||
private readonly ACCESS_TOKEN_KEY = 'auth_access_token';
|
||||
private readonly REFRESH_TOKEN_KEY = 'auth_refresh_token';
|
||||
private readonly TOKEN_TYPE_KEY = 'auth_token_type';
|
||||
private readonly EXPIRES_AT_KEY = 'auth_expires_at';
|
||||
|
||||
private tokenSubject = new BehaviorSubject<string | null>(this.getAccessToken());
|
||||
public token$ = this.tokenSubject.asObservable();
|
||||
|
||||
constructor() {
|
||||
// Check token validity on service initialization
|
||||
this.checkTokenValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token pair in localStorage
|
||||
*/
|
||||
setTokens(tokenPair: TokenPair): void {
|
||||
const expiresAt = Date.now() + (tokenPair.expires_in * 1000);
|
||||
|
||||
localStorage.setItem(this.ACCESS_TOKEN_KEY, tokenPair.access_token);
|
||||
localStorage.setItem(this.REFRESH_TOKEN_KEY, tokenPair.refresh_token);
|
||||
localStorage.setItem(this.TOKEN_TYPE_KEY, tokenPair.token_type);
|
||||
localStorage.setItem(this.EXPIRES_AT_KEY, expiresAt.toString());
|
||||
|
||||
this.tokenSubject.next(tokenPair.access_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token from localStorage
|
||||
*/
|
||||
getAccessToken(): string | null {
|
||||
return localStorage.getItem(this.ACCESS_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refresh token from localStorage
|
||||
*/
|
||||
getRefreshToken(): string | null {
|
||||
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token type from localStorage
|
||||
*/
|
||||
getTokenType(): string {
|
||||
return localStorage.getItem(this.TOKEN_TYPE_KEY) || 'Bearer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token expiration timestamp
|
||||
*/
|
||||
getExpiresAt(): number | null {
|
||||
const expiresAt = localStorage.getItem(this.EXPIRES_AT_KEY);
|
||||
return expiresAt ? parseInt(expiresAt, 10) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token exists
|
||||
*/
|
||||
hasToken(): boolean {
|
||||
return this.getAccessToken() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is expired
|
||||
*/
|
||||
isTokenExpired(): boolean {
|
||||
const expiresAt = this.getExpiresAt();
|
||||
if (!expiresAt) return true;
|
||||
|
||||
// Add 30 second buffer to account for clock skew
|
||||
return Date.now() >= (expiresAt - 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is valid (exists and not expired)
|
||||
*/
|
||||
isTokenValid(): boolean {
|
||||
return this.hasToken() && !this.isTokenExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization header value
|
||||
*/
|
||||
getAuthorizationHeader(): string | null {
|
||||
const token = this.getAccessToken();
|
||||
const tokenType = this.getTokenType();
|
||||
|
||||
return token ? `${tokenType} ${token}` : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tokens from storage
|
||||
*/
|
||||
clearTokens(): void {
|
||||
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(this.TOKEN_TYPE_KEY);
|
||||
localStorage.removeItem(this.EXPIRES_AT_KEY);
|
||||
|
||||
this.tokenSubject.next(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time until token expires (in milliseconds)
|
||||
*/
|
||||
getTimeUntilExpiry(): number {
|
||||
const expiresAt = this.getExpiresAt();
|
||||
if (!expiresAt) return 0;
|
||||
|
||||
const timeLeft = expiresAt - Date.now();
|
||||
return Math.max(0, timeLeft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token will expire soon (within 5 minutes)
|
||||
*/
|
||||
isTokenExpiringSoon(): boolean {
|
||||
const timeLeft = this.getTimeUntilExpiry();
|
||||
return timeLeft > 0 && timeLeft < 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token payload (without verification)
|
||||
*/
|
||||
decodeToken(token?: string): any {
|
||||
const tokenToDecoded = token || this.getAccessToken();
|
||||
if (!tokenToDecoded) return null;
|
||||
|
||||
try {
|
||||
const payload = tokenToDecoded.split('.')[1];
|
||||
const decoded = atob(payload);
|
||||
return JSON.parse(decoded);
|
||||
} catch (error) {
|
||||
console.warn('Failed to decode token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID from token
|
||||
*/
|
||||
getUserId(): string | null {
|
||||
const payload = this.decodeToken();
|
||||
return payload?.sub || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email from token
|
||||
*/
|
||||
getUserEmail(): string | null {
|
||||
const payload = this.decodeToken();
|
||||
return payload?.email || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user scopes from token
|
||||
*/
|
||||
getUserScopes(): string[] {
|
||||
const payload = this.decodeToken();
|
||||
return payload?.scopes || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user organization from token
|
||||
*/
|
||||
getUserOrganization(): string | null {
|
||||
const payload = this.decodeToken();
|
||||
return payload?.organization || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific scope
|
||||
*/
|
||||
hasScope(scope: string): boolean {
|
||||
const scopes = this.getUserScopes();
|
||||
return scopes.includes(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any of the specified scopes
|
||||
*/
|
||||
hasAnyScope(scopes: string[]): boolean {
|
||||
const userScopes = this.getUserScopes();
|
||||
return scopes.some(scope => userScopes.includes(scope));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has all of the specified scopes
|
||||
*/
|
||||
hasAllScopes(scopes: string[]): boolean {
|
||||
const userScopes = this.getUserScopes();
|
||||
return scopes.every(scope => userScopes.includes(scope));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check token validity and clear if invalid
|
||||
*/
|
||||
private checkTokenValidity(): void {
|
||||
if (this.hasToken() && this.isTokenExpired()) {
|
||||
this.clearTokens();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to storage events for cross-tab synchronization
|
||||
*/
|
||||
initStorageListener(): void {
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === this.ACCESS_TOKEN_KEY) {
|
||||
this.tokenSubject.next(event.newValue);
|
||||
} else if (event.key === null) {
|
||||
// Storage was cleared
|
||||
this.tokenSubject.next(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
22
projects/auth-client/src/public-api.ts
Normal file
22
projects/auth-client/src/public-api.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Public API Surface of auth-client
|
||||
*/
|
||||
|
||||
// Models
|
||||
export * from './lib/models/auth.models';
|
||||
|
||||
// Services
|
||||
export * from './lib/services/auth.service';
|
||||
export * from './lib/services/auth-http.service';
|
||||
export * from './lib/services/token.service';
|
||||
export * from './lib/services/oauth.service';
|
||||
|
||||
// Guards
|
||||
export * from './lib/guards/auth.guard';
|
||||
export * from './lib/guards/guest.guard';
|
||||
|
||||
// Interceptors
|
||||
export * from './lib/interceptors/auth.interceptor';
|
||||
|
||||
// Legacy export for compatibility
|
||||
export * from './lib/auth-client';
|
||||
18
projects/auth-client/tsconfig.lib.json
Normal file
18
projects/auth-client/tsconfig.lib.json
Normal file
@@ -0,0 +1,18 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
14
projects/auth-client/tsconfig.spec.json
Normal file
14
projects/auth-client/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -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 { routes } from './app.routes';
|
||||
import { UiAccessibilityModule } from '../../../ui-accessibility/src/lib/ui-accessibility.module';
|
||||
|
||||
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,208 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiAccessibilityModule } from '../../../../../ui-accessibility/src/lib/ui-accessibility.module';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
import { LiveAnnouncerService } from '../../../../../ui-accessibility/src/lib/services/live-announcer/live-announcer.service';
|
||||
import { FocusMonitorService } from '../../../../../ui-accessibility/src/lib/services/focus-monitor/focus-monitor.service';
|
||||
import { KeyboardManagerService } from '../../../../../ui-accessibility/src/lib/services/keyboard-manager/keyboard-manager.service';
|
||||
import { HighContrastService } from '../../../../../ui-accessibility/src/lib/services/high-contrast/high-contrast.service';
|
||||
import { A11yConfigService } from '../../../../../ui-accessibility/src/lib/services/a11y-config/a11y-config.service';
|
||||
|
||||
|
||||
@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,224 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiAnimationsService } from '../../../../../ui-animations/src/lib/ui-animations.service';
|
||||
import { AnimateDirective } from '../../../../../ui-animations/src/lib/animate.directive';
|
||||
|
||||
@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,303 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BackgroundDirective } from '../../../../../ui-backgrounds/src/lib/directives/background.directive';
|
||||
import { SolidBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/solid-background.component';
|
||||
import { GradientBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/gradient-background.component';
|
||||
import { PatternBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/pattern-background.component';
|
||||
import { ImageBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/image-background.component';
|
||||
import { LinearGradientConfig, PatternConfig, SolidBackgroundConfig } from '../../../../../ui-backgrounds/src/lib/types/background.types';
|
||||
import { BackgroundService } from '../../../../../ui-backgrounds/src/lib/services/background.service';
|
||||
|
||||
|
||||
@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,211 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CodeSnippetComponent } from '../../../../../ui-code-display/src/lib/components/code-snippet/code-snippet.component';
|
||||
import { InlineCodeComponent } from '../../../../../ui-code-display/src/lib/components/inline-code/inline-code.component';
|
||||
import { CodeBlockComponent } from '../../../../../ui-code-display/src/lib/components/code-block/code-block.component';
|
||||
import { CodeTheme, CodeThemeService } from '../../../../../ui-code-display/src/lib/services/theme.service';
|
||||
|
||||
@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,143 @@
|
||||
@use 'ui-design-system/src/styles/semantic' as semantic;
|
||||
|
||||
.conversion-demo {
|
||||
min-height: 100vh;
|
||||
background: semantic.$semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-lg semantic.$semantic-spacing-container-card-padding;
|
||||
|
||||
&:first-child {
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, semantic.$semantic-color-primary-container 0%, semantic.$semantic-color-tertiary-container 100%);
|
||||
border-bottom: 1px solid semantic.$semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-bottom: 1px solid semantic.$semantic-color-border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 1.125rem;
|
||||
color: semantic.$semantic-color-text-secondary;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 600;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-section-description {
|
||||
font-size: 1.125rem;
|
||||
color: semantic.$semantic-color-text-secondary;
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto semantic.$semantic-spacing-component-xl auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: semantic.$semantic-spacing-layout-section-sm;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: semantic.$semantic-spacing-component-xl;
|
||||
margin-bottom: semantic.$semantic-spacing-layout-section-sm;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-demo {
|
||||
background: semantic.$semantic-color-primary;
|
||||
padding: semantic.$semantic-spacing-component-xl;
|
||||
border-radius: 0.5rem;
|
||||
color: semantic.$semantic-color-on-primary;
|
||||
}
|
||||
|
||||
// Component spacing adjustments
|
||||
:host ::ng-deep {
|
||||
ui-cta-section {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-pricing-table {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-newsletter-signup {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-contact-form {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-sm semantic.$semantic-spacing-container-card-padding;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-xs semantic.$semantic-spacing-container-card-padding;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-example-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
// import {
|
||||
// CTASectionComponent,
|
||||
// PricingTableComponent,
|
||||
// NewsletterSignupComponent,
|
||||
// ContactFormComponent
|
||||
// } from 'ui-landing-pages';
|
||||
// import {
|
||||
// CTASectionConfig,
|
||||
// PricingTableConfig,
|
||||
// NewsletterSignupConfig,
|
||||
// ContactFormConfig
|
||||
// } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conversion-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
// CTASectionComponent,
|
||||
// PricingTableComponent,
|
||||
// NewsletterSignupComponent,
|
||||
// ContactFormComponent
|
||||
],
|
||||
template: `
|
||||
<div class="conversion-demo">
|
||||
<div class="demo-section">
|
||||
<h1 class="demo-title">Conversion Components Demo</h1>
|
||||
<p class="demo-description">
|
||||
Phase 3 components focused on user actions and conversions, including CTAs, pricing tables, newsletter signups, and contact forms.
|
||||
</p>
|
||||
<div class="under-construction">
|
||||
<p><strong>🚧 Under Construction:</strong> This demo is temporarily disabled while the ui-landing-pages library is being built.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temporarily commented out until ui-landing-pages is built -->
|
||||
`,
|
||||
styleUrls: ['./conversion-demo.component.scss']
|
||||
})
|
||||
export class ConversionDemoComponent {
|
||||
|
||||
// CTA Section Configurations
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
/*
|
||||
ctaConfigGradient: CTASectionConfig = {
|
||||
title: "Transform Your Business Today",
|
||||
description: "Join thousands of companies already using our platform to accelerate growth and increase efficiency.",
|
||||
backgroundType: 'gradient',
|
||||
ctaPrimary: {
|
||||
text: 'Start Free Trial',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Primary CTA clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'Watch Demo',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Secondary CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'countdown',
|
||||
text: 'Limited Time Offer Ends In:',
|
||||
endDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000) // 5 days from now
|
||||
}
|
||||
};
|
||||
|
||||
ctaConfigPattern: CTASectionConfig = {
|
||||
title: "Don't Miss Out on This Exclusive Deal",
|
||||
description: "Get 50% off your first year and unlock premium features.",
|
||||
backgroundType: 'pattern',
|
||||
ctaPrimary: {
|
||||
text: 'Claim Offer Now',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Pattern CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'limited-offer',
|
||||
text: 'Flash Sale',
|
||||
remaining: 12
|
||||
}
|
||||
};
|
||||
|
||||
ctaConfigSolid: CTASectionConfig = {
|
||||
title: "Join Over 10,000 Happy Customers",
|
||||
description: "See why businesses trust us with their most important processes.",
|
||||
backgroundType: 'solid',
|
||||
ctaPrimary: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Solid CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'social-proof',
|
||||
text: '🔥 142 people signed up in the last 24 hours'
|
||||
}
|
||||
};
|
||||
|
||||
// Pricing Table Configuration
|
||||
pricingConfig: PricingTableConfig = {
|
||||
billingToggle: {
|
||||
monthlyLabel: 'Monthly',
|
||||
yearlyLabel: 'Yearly',
|
||||
discountText: 'Save 20%'
|
||||
},
|
||||
featuresComparison: true,
|
||||
highlightedPlan: 'pro',
|
||||
plans: [
|
||||
{
|
||||
id: 'starter',
|
||||
name: 'Starter',
|
||||
description: 'Perfect for small teams getting started',
|
||||
price: {
|
||||
monthly: 29,
|
||||
yearly: 24,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Up to 5 team members', included: true },
|
||||
{ name: '10GB storage', included: true },
|
||||
{ name: 'Basic reporting', included: true },
|
||||
{ name: 'Email support', included: true },
|
||||
{ name: 'Advanced analytics', included: false },
|
||||
{ name: 'API access', included: false },
|
||||
{ name: 'Priority support', included: false }
|
||||
],
|
||||
cta: {
|
||||
text: 'Start Free Trial',
|
||||
action: () => console.log('Starter plan selected')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Professional',
|
||||
description: 'Best for growing businesses',
|
||||
badge: 'Most Popular',
|
||||
popular: true,
|
||||
price: {
|
||||
monthly: 79,
|
||||
yearly: 63,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Up to 25 team members', included: true },
|
||||
{ name: '100GB storage', included: true },
|
||||
{ name: 'Advanced reporting', included: true, highlight: true },
|
||||
{ name: 'Email & chat support', included: true },
|
||||
{ name: 'Advanced analytics', included: true, highlight: true },
|
||||
{ name: 'API access', included: true },
|
||||
{ name: 'Priority support', included: false }
|
||||
],
|
||||
cta: {
|
||||
text: 'Start Free Trial',
|
||||
action: () => console.log('Pro plan selected')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For large organizations with custom needs',
|
||||
price: {
|
||||
monthly: 199,
|
||||
yearly: 159,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Unlimited team members', included: true },
|
||||
{ name: 'Unlimited storage', included: true },
|
||||
{ name: 'Custom reporting', included: true },
|
||||
{ name: '24/7 phone support', included: true },
|
||||
{ name: 'Advanced analytics', included: true },
|
||||
{ name: 'Full API access', included: true },
|
||||
{ name: 'Priority support', included: true, highlight: true }
|
||||
],
|
||||
cta: {
|
||||
text: 'Contact Sales',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Enterprise plan selected')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Newsletter Signup Configurations
|
||||
newsletterConfigInline: NewsletterSignupConfig = {
|
||||
title: "Stay Updated with Our Newsletter",
|
||||
description: "Get the latest insights, tips, and updates delivered to your inbox.",
|
||||
placeholder: "Enter your email address",
|
||||
ctaText: "Subscribe",
|
||||
variant: 'inline',
|
||||
successMessage: "Thanks for subscribing! Check your email for confirmation."
|
||||
};
|
||||
|
||||
newsletterConfigModal: NewsletterSignupConfig = {
|
||||
title: "Join Our Community",
|
||||
description: "Be the first to know about new features and exclusive content.",
|
||||
placeholder: "Your email address",
|
||||
ctaText: "Join Now",
|
||||
variant: 'modal',
|
||||
successMessage: "Welcome aboard! You're now part of our community."
|
||||
};
|
||||
|
||||
newsletterConfigFooter: NewsletterSignupConfig = {
|
||||
title: "Newsletter Signup",
|
||||
description: "Weekly insights and updates from our team.",
|
||||
placeholder: "Email address",
|
||||
ctaText: "Sign Up",
|
||||
variant: 'footer',
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "We respect your privacy and will never spam you.",
|
||||
successMessage: "Successfully subscribed! Welcome to our newsletter."
|
||||
};
|
||||
|
||||
// Contact Form Configurations
|
||||
contactConfigTwoColumn: ContactFormConfig = {
|
||||
title: "Get in Touch",
|
||||
description: "We'd love to hear from you. Send us a message and we'll respond as soon as possible.",
|
||||
layout: 'two-column',
|
||||
submitText: 'Send Message',
|
||||
successMessage: "Thank you for your message! We'll get back to you within 24 hours.",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'firstName',
|
||||
label: 'First Name',
|
||||
placeholder: 'John',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'lastName',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Doe',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'john@example.com',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'tel',
|
||||
name: 'phone',
|
||||
label: 'Phone Number',
|
||||
placeholder: '+1 (555) 123-4567',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'subject',
|
||||
label: 'Subject',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'general', label: 'General Inquiry' },
|
||||
{ value: 'support', label: 'Technical Support' },
|
||||
{ value: 'sales', label: 'Sales Question' },
|
||||
{ value: 'partnership', label: 'Partnership' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'message',
|
||||
label: 'Message',
|
||||
placeholder: 'Tell us more about your inquiry...',
|
||||
required: true,
|
||||
rows: 5
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
contactConfigSingleColumn: ContactFormConfig = {
|
||||
title: "Quick Contact",
|
||||
layout: 'single-column',
|
||||
submitText: 'Submit',
|
||||
successMessage: "Message sent successfully!",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Full Name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email Address',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'message',
|
||||
label: 'Your Message',
|
||||
required: true,
|
||||
rows: 4
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'newsletter',
|
||||
label: 'Subscribe to our newsletter for updates',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
contactConfigInline: ContactFormConfig = {
|
||||
layout: 'inline',
|
||||
submitText: 'Contact Us',
|
||||
successMessage: "We'll be in touch soon!",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
placeholder: 'Your name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'your@email.com',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
placeholder: 'Your company',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
onNewsletterSignup(data: any) {
|
||||
console.log('Newsletter signup:', data);
|
||||
}
|
||||
|
||||
onContactFormSubmit(data: any) {
|
||||
console.log('Contact form submission:', data);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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,733 @@
|
||||
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FilterConfig, SortConfig, GroupByResult } from '../../../../../ui-data-utils/src/lib/types';
|
||||
import { sortBy, sortByMultiple } from '../../../../../ui-data-utils/src/lib/sorting';
|
||||
import { filterBy, filterByMultiple, searchFilter } from '../../../../../ui-data-utils/src/lib/filtering';
|
||||
import { getPaginationRange, paginate } from '../../../../../ui-data-utils/src/lib/pagination';
|
||||
import { aggregate, groupBy, pivot, pluck, unique } from '../../../../../ui-data-utils/src/lib/transformation';
|
||||
|
||||
|
||||
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: SampleData) => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
|
||||
}
|
||||
|
||||
getMultiSortedDataPreview(): string {
|
||||
return JSON.stringify(this.multiSortedData().slice(0, 4).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getSearchFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.searchFilteredData().slice(0, 3).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPropertyFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPaginatedDataPreview(): string {
|
||||
return JSON.stringify(this.paginationResult().data.map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getGroupedDataPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
return JSON.stringify(grouped.map((group: GroupByResult<SampleData>) => ({
|
||||
department: group.key,
|
||||
count: group.count,
|
||||
employees: group.items.map((emp: SampleData) => emp.name)
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getAggregationPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
const result = grouped.map((group: GroupByResult<SampleData>) => ({
|
||||
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: SampleData) => ({
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
561
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
561
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
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 { 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 { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.component';
|
||||
import { ConversionDemoComponent } from './conversion-demo/conversion-demo.component';
|
||||
import { LandingHeaderDemoComponent } from './landing-header-demo/landing-header-demo.component';
|
||||
import { LandingFooterDemoComponent } from './landing-footer-demo/landing-footer-demo.component';
|
||||
import { LandingFAQDemoComponent } from './landing-faq-demo/landing-faq-demo.component';
|
||||
import { LandingTemplatesDemoComponent } from "./landing-templates-demo/landing-templates-demo.component";
|
||||
import { LandingTimelineDemoComponent } from "./landing-timeline-demo/landing-timeline-demo.component";
|
||||
import { LandingTeamDemoComponent } from "./landing-team-demo/landing-team-demo.component";
|
||||
import { LandingStatisticsDemoComponent } from "./landing-statistics-demo/landing-statistics-demo.component";
|
||||
import { LandingFeatureGridDemoComponent } from "./landing-feature-grid-demo/landing-feature-grid-demo.component";
|
||||
import { LandingTestimonialsDemoComponent } from "./landing-testimonials-demo/landing-testimonials-demo.component";
|
||||
import { LandingLogoCloudDemoComponent } from "./landing-logo-cloud-demo/landing-logo-cloud-demo.component";
|
||||
import { AnimationsDemoComponent } from "./animations-demo/animations-demo.component";
|
||||
import { CodeDisplayDemoComponent } from "./code-display-demo/code-display-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") {
|
||||
<app-video-player-demo></app-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>
|
||||
}
|
||||
|
||||
@case ("conversion") {
|
||||
<app-conversion-demo></app-conversion-demo>
|
||||
}
|
||||
|
||||
@case ("landing-faq") {
|
||||
<app-landing-faq-demo></app-landing-faq-demo>
|
||||
}
|
||||
|
||||
@case ("landing-feature-grid") {
|
||||
<app-landing-feature-grid-demo></app-landing-feature-grid-demo>
|
||||
}
|
||||
|
||||
@case ("landing-testimonials") {
|
||||
<app-landing-testimonials-demo></app-landing-testimonials-demo>
|
||||
}
|
||||
|
||||
@case ("landing-logo-cloud") {
|
||||
<app-landing-logo-cloud-demo></app-landing-logo-cloud-demo>
|
||||
}
|
||||
|
||||
@case ("landing-statistics") {
|
||||
<app-landing-statistics-demo></app-landing-statistics-demo>
|
||||
}
|
||||
|
||||
@case ("landing-header") {
|
||||
<app-landing-header-demo></app-landing-header-demo>
|
||||
}
|
||||
|
||||
@case ("landing-footer") {
|
||||
<app-landing-footer-demo></app-landing-footer-demo>
|
||||
}
|
||||
|
||||
@case ("landing-team") {
|
||||
<app-landing-team-demo></app-landing-team-demo>
|
||||
}
|
||||
|
||||
@case ("landing-timeline") {
|
||||
<app-landing-timeline-demo></app-landing-timeline-demo>
|
||||
}
|
||||
|
||||
@case ("landing-templates") {
|
||||
<app-landing-templates-demo></app-landing-templates-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, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent,
|
||||
FontManagerDemoComponent, BackgroundsDemoComponent,
|
||||
ConversionDemoComponent, LandingFAQDemoComponent,
|
||||
LandingFeatureGridDemoComponent, LandingTestimonialsDemoComponent, LandingLogoCloudDemoComponent, LandingStatisticsDemoComponent, LandingHeaderDemoComponent,
|
||||
LandingFooterDemoComponent, LandingTeamDemoComponent, LandingTimelineDemoComponent, LandingTemplatesDemoComponent,
|
||||
LandingTemplatesDemoComponent, LandingTimelineDemoComponent, LandingTeamDemoComponent, LandingFooterDemoComponent, LandingHeaderDemoComponent, LandingStatisticsDemoComponent,
|
||||
LandingFeatureGridDemoComponent, LandingTestimonialsDemoComponent, LandingLogoCloudDemoComponent, AnimationsDemoComponent, CodeDisplayDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user