Add comprehensive library expansion with new components and demos
- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio - Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout - Add comprehensive demo components for all new features - Update project configuration and dependencies - Expand component exports and routing structure - Add UI landing pages planning document 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,48 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { UiAccessibilityModule } from 'ui-accessibility';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
importProvidersFrom(
|
||||
UiAccessibilityModule.forRoot({
|
||||
announcer: {
|
||||
defaultPoliteness: 'polite',
|
||||
defaultDuration: 4000,
|
||||
enabled: true
|
||||
},
|
||||
focusManagement: {
|
||||
trapFocus: true,
|
||||
restoreFocus: true,
|
||||
focusVisibleEnabled: true
|
||||
},
|
||||
keyboard: {
|
||||
enableShortcuts: true,
|
||||
enableArrowNavigation: true
|
||||
},
|
||||
skipLinks: {
|
||||
enabled: true,
|
||||
position: 'top',
|
||||
links: [
|
||||
{ href: '#main', text: 'Skip to main content' },
|
||||
{ href: '#nav', text: 'Skip to navigation' }
|
||||
]
|
||||
},
|
||||
accessibility: {
|
||||
respectReducedMotion: true,
|
||||
respectHighContrast: true,
|
||||
injectAccessibilityStyles: true,
|
||||
addBodyClasses: true
|
||||
},
|
||||
development: {
|
||||
warnings: true,
|
||||
logging: true
|
||||
}
|
||||
})
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<div class="accessibility-demo">
|
||||
<!-- Skip Links Demo -->
|
||||
<ui-skip-links></ui-skip-links>
|
||||
|
||||
<header class="demo-header">
|
||||
<h1 id="main">UI Accessibility Library Demo</h1>
|
||||
<p>Interactive examples of accessibility features with WCAG 2.1 Level AA compliance.</p>
|
||||
|
||||
<ui-screen-reader-only>
|
||||
This page demonstrates various accessibility features. Use keyboard shortcuts: Ctrl+Shift+A to make announcements, Ctrl+M to toggle modal.
|
||||
</ui-screen-reader-only>
|
||||
</header>
|
||||
|
||||
<!-- Current Preferences Display -->
|
||||
<section class="demo-section" aria-labelledby="preferences-heading">
|
||||
<h2 id="preferences-heading">Accessibility Preferences</h2>
|
||||
<div class="preferences-display">
|
||||
<p><strong>High Contrast:</strong> {{ currentPreferences.prefersHighContrast ? 'Enabled' : 'Disabled' }}</p>
|
||||
<p><strong>Reduced Motion:</strong> {{ currentPreferences.prefersReducedMotion ? 'Enabled' : 'Disabled' }}</p>
|
||||
<p><strong>Color Scheme:</strong> {{ currentPreferences.colorScheme || 'Not detected' }}</p>
|
||||
<ui-button variant="tonal" size="small" (click)="refreshPreferences()">
|
||||
Refresh Preferences
|
||||
</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live Announcements Demo -->
|
||||
<section class="demo-section" aria-labelledby="announcements-heading">
|
||||
<h2 id="announcements-heading">Live Announcements</h2>
|
||||
<div class="demo-controls">
|
||||
<div class="input-group">
|
||||
<label for="announcement-text">Announcement Text:</label>
|
||||
<input
|
||||
id="announcement-text"
|
||||
type="text"
|
||||
[(ngModel)]="announcementText"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="politeness-level">Politeness Level:</label>
|
||||
<select id="politeness-level" [(ngModel)]="selectedPoliteness" class="demo-select">
|
||||
<option value="polite">Polite</option>
|
||||
<option value="assertive">Assertive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button (click)="makeAnnouncement()">Make Announcement</ui-button>
|
||||
<ui-button variant="tonal" (click)="makeSequenceAnnouncement()">Sequence Announcement</ui-button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button variant="filled" (click)="announceSuccess()">Success Message</ui-button>
|
||||
<ui-button variant="outlined" (click)="announceError()">Error Message</ui-button>
|
||||
<ui-button variant="tonal" (click)="announceLoading()">Loading Message</ui-button>
|
||||
<ui-button variant="outlined" (click)="simulateFormValidation()">Simulate Validation</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Focus Management Demo -->
|
||||
<section class="demo-section" aria-labelledby="focus-heading">
|
||||
<h2 id="focus-heading">Focus Management</h2>
|
||||
|
||||
<div class="focus-demo">
|
||||
<div class="input-group">
|
||||
<label for="monitored-input">Monitored Input (Focus Origin: {{ focusOrigin }}):</label>
|
||||
<input
|
||||
#monitoredInput
|
||||
id="monitored-input"
|
||||
type="text"
|
||||
placeholder="Click, tab, or programmatically focus this input"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button (click)="focusViaKeyboard()">Focus via Keyboard</ui-button>
|
||||
<ui-button (click)="focusViaMouse()">Focus via Mouse</ui-button>
|
||||
<ui-button (click)="focusViaProgram()">Focus via Program</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Focus Trap Demo -->
|
||||
<section class="demo-section" aria-labelledby="trap-heading">
|
||||
<h2 id="trap-heading">Focus Trap</h2>
|
||||
|
||||
<div class="trap-controls">
|
||||
<ui-button (click)="toggleFocusTrap()">
|
||||
{{ trapEnabled ? 'Disable' : 'Enable' }} Focus Trap
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
#trapContainer
|
||||
class="focus-trap-container"
|
||||
[class.trap-enabled]="trapEnabled"
|
||||
[uiFocusTrap]="trapEnabled"
|
||||
[autoFocus]="true"
|
||||
[restoreFocus]="true"
|
||||
>
|
||||
<h3>Focus Trap Container {{ trapEnabled ? '(Active)' : '(Inactive)' }}</h3>
|
||||
<p>When enabled, Tab navigation will be trapped within this container.</p>
|
||||
|
||||
<input type="text" placeholder="First input" class="demo-input">
|
||||
<input type="text" placeholder="Second input" class="demo-input">
|
||||
<ui-button>Trapped Button 1</ui-button>
|
||||
<ui-button variant="tonal">Trapped Button 2</ui-button>
|
||||
<select class="demo-select">
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
</select>
|
||||
<textarea placeholder="Textarea" class="demo-textarea"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Arrow Navigation Demo -->
|
||||
<section class="demo-section" aria-labelledby="navigation-heading">
|
||||
<h2 id="navigation-heading">Arrow Navigation</h2>
|
||||
|
||||
<div class="navigation-demo">
|
||||
<h3>Vertical Navigation (Use ↑↓ arrows):</h3>
|
||||
<ul
|
||||
class="navigation-list vertical"
|
||||
uiArrowNavigation="vertical"
|
||||
[wrap]="true"
|
||||
(navigationChange)="onNavigationChange($event)"
|
||||
>
|
||||
@for (item of navigationItems; track item.id) {
|
||||
<li
|
||||
[tabindex]="$first ? '0' : '-1'"
|
||||
[class.disabled]="item.disabled"
|
||||
[attr.aria-disabled]="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<h3>Grid Navigation (Use ↑↓←→ arrows, 3 columns):</h3>
|
||||
<div
|
||||
class="navigation-grid"
|
||||
uiArrowNavigation="grid"
|
||||
[gridColumns]="3"
|
||||
[wrap]="true"
|
||||
(navigationChange)="onGridNavigationChange($event)"
|
||||
>
|
||||
@for (item of gridItems; track item.id) {
|
||||
<button
|
||||
class="grid-item"
|
||||
[tabindex]="$first ? '0' : '-1'"
|
||||
[disabled]="item.disabled"
|
||||
[attr.aria-disabled]="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal Demo -->
|
||||
<section class="demo-section" aria-labelledby="modal-heading">
|
||||
<h2 id="modal-heading">Modal with Focus Trap</h2>
|
||||
|
||||
<ui-button (click)="toggleModal()">Open Modal</ui-button>
|
||||
|
||||
@if (showModal) {
|
||||
<div class="modal-backdrop" (click)="toggleModal()">
|
||||
<div
|
||||
class="modal-content"
|
||||
[uiFocusTrap]="true"
|
||||
[autoFocus]="true"
|
||||
[restoreFocus]="true"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
aria-modal="true"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<header class="modal-header">
|
||||
<h3 id="modal-title">Demo Modal</h3>
|
||||
<button
|
||||
class="modal-close"
|
||||
(click)="toggleModal()"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>This modal demonstrates focus trapping. Tab navigation is contained within the modal.</p>
|
||||
|
||||
<input type="text" placeholder="Modal input" class="demo-input">
|
||||
<ui-button variant="tonal">Modal Button</ui-button>
|
||||
</div>
|
||||
|
||||
<footer class="modal-footer">
|
||||
<ui-button (click)="toggleModal()">Close</ui-button>
|
||||
<ui-button variant="filled">Save Changes</ui-button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Screen Reader Components -->
|
||||
<section class="demo-section" aria-labelledby="sr-heading">
|
||||
<h2 id="sr-heading">Screen Reader Components</h2>
|
||||
|
||||
<div class="sr-demo">
|
||||
<h3>Screen Reader Only Text:</h3>
|
||||
<p>
|
||||
This text is visible.
|
||||
<ui-screen-reader-only>This text is only for screen readers.</ui-screen-reader-only>
|
||||
This text is visible again.
|
||||
</p>
|
||||
|
||||
<h3>Focusable Screen Reader Text:</h3>
|
||||
<ui-screen-reader-only type="focusable">
|
||||
This text becomes visible when focused (try tabbing through the page).
|
||||
</ui-screen-reader-only>
|
||||
|
||||
<h3>Dynamic Status Messages:</h3>
|
||||
<div class="status-demo">
|
||||
<ui-screen-reader-only type="status" statusType="success">
|
||||
Form submitted successfully!
|
||||
</ui-screen-reader-only>
|
||||
|
||||
<ui-screen-reader-only type="status" statusType="error">
|
||||
Error: Please check your input.
|
||||
</ui-screen-reader-only>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Keyboard Shortcuts Info -->
|
||||
<section class="demo-section" aria-labelledby="shortcuts-heading">
|
||||
<h2 id="shortcuts-heading">Keyboard Shortcuts</h2>
|
||||
|
||||
<div class="shortcuts-info">
|
||||
<dl class="shortcuts-list">
|
||||
<dt>Ctrl + Shift + A</dt>
|
||||
<dd>Make announcement</dd>
|
||||
|
||||
<dt>Ctrl + M</dt>
|
||||
<dd>Toggle modal</dd>
|
||||
|
||||
<dt>Tab / Shift + Tab</dt>
|
||||
<dd>Navigate through interactive elements</dd>
|
||||
|
||||
<dt>Arrow Keys</dt>
|
||||
<dd>Navigate within lists and grids (when focused)</dd>
|
||||
|
||||
<dt>Home / End</dt>
|
||||
<dd>Jump to first/last item in navigable containers</dd>
|
||||
|
||||
<dt>Escape</dt>
|
||||
<dd>Close modal or exit focus trap</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="demo-footer">
|
||||
<p>
|
||||
<ui-screen-reader-only>End of accessibility demo.</ui-screen-reader-only>
|
||||
This demo showcases WCAG 2.1 Level AA compliant accessibility features using semantic design tokens.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,476 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.accessibility-demo {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-component-lg * 2;
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-component-lg * 2;
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
h2 {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-lg / 2 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Preferences Display
|
||||
.preferences-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Controls
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #1a1a1a;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.15s ease-out;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
@extend .demo-input;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-textarea {
|
||||
@extend .demo-input;
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
// Focus Demo
|
||||
.focus-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Focus Trap Demo
|
||||
.trap-controls {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.focus-trap-container {
|
||||
padding: $semantic-spacing-component-md * 1.5;
|
||||
border: 2px dashed #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
transition: all 0.3s ease-out;
|
||||
|
||||
&.trap-enabled {
|
||||
border-color: #007bff;
|
||||
background: #f0f8ff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Demo
|
||||
.navigation-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.navigation-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
color: #1a1a1a;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: -2px;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
|
||||
.grid-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #f8f9fa;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Demo
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
|
||||
.modal-header {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease-out;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: $semantic-spacing-component-md * 1.5;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
width: 100%;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Screen Reader Demo
|
||||
.sr-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin: $semantic-spacing-component-lg / 2 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts
|
||||
.shortcuts-info {
|
||||
background: #f5f5f5;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
margin: 0;
|
||||
|
||||
dt {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
background: #e9ecef;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
.demo-footer {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-md * 2;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: $semantic-spacing-component-lg * 2;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.accessibility-demo {
|
||||
padding: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
dt {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
min-width: 90vw;
|
||||
margin: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// High Contrast Mode Support
|
||||
@media (prefers-contrast: high) {
|
||||
.demo-section {
|
||||
border-color: ButtonText;
|
||||
}
|
||||
|
||||
.focus-trap-container.trap-enabled {
|
||||
border-color: Highlight;
|
||||
}
|
||||
|
||||
.navigation-list li:focus {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
|
||||
.grid-item:focus {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced Motion Support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01s !important;
|
||||
transition-duration: 0.01s !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccessibilityDemoComponent } from './accessibility-demo.component';
|
||||
|
||||
describe('AccessibilityDemoComponent', () => {
|
||||
let component: AccessibilityDemoComponent;
|
||||
let fixture: ComponentFixture<AccessibilityDemoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AccessibilityDemoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccessibilityDemoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
// Import UI Accessibility components and services
|
||||
import {
|
||||
UiAccessibilityModule,
|
||||
LiveAnnouncerService,
|
||||
FocusMonitorService,
|
||||
KeyboardManagerService,
|
||||
HighContrastService,
|
||||
A11yConfigService
|
||||
} from 'ui-accessibility';
|
||||
|
||||
// Import UI Essentials components
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accessibility-demo',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
UiAccessibilityModule,
|
||||
ButtonComponent
|
||||
],
|
||||
templateUrl: './accessibility-demo.component.html',
|
||||
styleUrl: './accessibility-demo.component.scss'
|
||||
})
|
||||
export class AccessibilityDemoComponent implements OnInit, OnDestroy {
|
||||
private liveAnnouncer = inject(LiveAnnouncerService);
|
||||
private focusMonitor = inject(FocusMonitorService);
|
||||
private keyboardManager = inject(KeyboardManagerService);
|
||||
private highContrast = inject(HighContrastService);
|
||||
private a11yConfig = inject(A11yConfigService);
|
||||
|
||||
@ViewChild('trapContainer') trapContainer!: ElementRef;
|
||||
@ViewChild('monitoredInput') monitoredInput!: ElementRef;
|
||||
|
||||
// Demo state
|
||||
trapEnabled = false;
|
||||
showModal = false;
|
||||
currentPreferences: any = {};
|
||||
focusOrigin = '';
|
||||
announcementText = 'Hello, this is a test announcement!';
|
||||
selectedPoliteness: 'polite' | 'assertive' = 'polite';
|
||||
|
||||
// Navigation demo items
|
||||
navigationItems = [
|
||||
{ id: 1, label: 'Profile Settings', disabled: false },
|
||||
{ id: 2, label: 'Account Details', disabled: false },
|
||||
{ id: 3, label: 'Privacy Controls', disabled: true },
|
||||
{ id: 4, label: 'Notifications', disabled: false },
|
||||
{ id: 5, label: 'Security Options', disabled: false }
|
||||
];
|
||||
|
||||
// Grid navigation items
|
||||
gridItems = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
label: `Item ${i + 1}`,
|
||||
disabled: i === 5 // Make item 6 disabled
|
||||
}));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupKeyboardShortcuts();
|
||||
this.setupFocusMonitoring();
|
||||
this.getCurrentPreferences();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.keyboardManager.unregisterAll();
|
||||
}
|
||||
|
||||
private setupKeyboardShortcuts(): void {
|
||||
// Global shortcuts
|
||||
this.keyboardManager.registerGlobalShortcut('announce', {
|
||||
key: 'a',
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
description: 'Make announcement',
|
||||
handler: () => this.makeAnnouncement()
|
||||
});
|
||||
|
||||
this.keyboardManager.registerGlobalShortcut('toggleModal', {
|
||||
key: 'm',
|
||||
ctrlKey: true,
|
||||
description: 'Toggle modal',
|
||||
handler: () => this.toggleModal()
|
||||
});
|
||||
}
|
||||
|
||||
private setupFocusMonitoring(): void {
|
||||
// Monitor focus on the input field
|
||||
setTimeout(() => {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.monitor(this.monitoredInput).subscribe(focusEvent => {
|
||||
this.focusOrigin = focusEvent.origin || 'none';
|
||||
if (focusEvent.origin) {
|
||||
this.liveAnnouncer.announce(
|
||||
`Input focused via ${focusEvent.origin}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private getCurrentPreferences(): void {
|
||||
this.currentPreferences = this.highContrast.getCurrentPreferences();
|
||||
}
|
||||
|
||||
// Demo actions
|
||||
makeAnnouncement(): void {
|
||||
this.liveAnnouncer.announce(this.announcementText, this.selectedPoliteness);
|
||||
}
|
||||
|
||||
makeSequenceAnnouncement(): void {
|
||||
const messages = [
|
||||
'Starting process...',
|
||||
'Processing data...',
|
||||
'Validating information...',
|
||||
'Process completed successfully!'
|
||||
];
|
||||
this.liveAnnouncer.announceSequence(messages, 'polite', 1500);
|
||||
}
|
||||
|
||||
toggleFocusTrap(): void {
|
||||
this.trapEnabled = !this.trapEnabled;
|
||||
this.liveAnnouncer.announce(
|
||||
`Focus trap ${this.trapEnabled ? 'enabled' : 'disabled'}`,
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
|
||||
toggleModal(): void {
|
||||
this.showModal = !this.showModal;
|
||||
if (this.showModal) {
|
||||
this.liveAnnouncer.announce('Modal opened', 'assertive');
|
||||
} else {
|
||||
this.liveAnnouncer.announce('Modal closed', 'assertive');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaKeyboard(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'keyboard');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaMouse(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'mouse');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaProgram(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'program');
|
||||
}
|
||||
}
|
||||
|
||||
onNavigationChange(event: any): void {
|
||||
this.liveAnnouncer.announce(
|
||||
`Navigated to ${this.navigationItems[event.nextIndex]?.label}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
onGridNavigationChange(event: any): void {
|
||||
this.liveAnnouncer.announce(
|
||||
`Navigated to ${this.gridItems[event.nextIndex]?.label}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
announceError(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Error: Please fix the required fields before continuing',
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
|
||||
announceSuccess(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Success: Your changes have been saved',
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
announceLoading(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Loading content, please wait...',
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
refreshPreferences(): void {
|
||||
this.getCurrentPreferences();
|
||||
this.liveAnnouncer.announce('Accessibility preferences updated', 'polite');
|
||||
}
|
||||
|
||||
simulateFormValidation(): void {
|
||||
// Simulate a form validation scenario
|
||||
this.liveAnnouncer.announce('Validating form...', 'polite');
|
||||
|
||||
setTimeout(() => {
|
||||
this.liveAnnouncer.announce(
|
||||
'Form validation complete. 2 errors found.',
|
||||
'assertive'
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiAnimationsService, AnimateDirective } from 'ui-animations';
|
||||
|
||||
@Component({
|
||||
selector: 'app-animations-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AnimateDirective],
|
||||
template: `
|
||||
<div class="animations-demo">
|
||||
<h2>UI Animations Demo</h2>
|
||||
|
||||
<!-- Entrance Animations -->
|
||||
<section class="demo-section">
|
||||
<h3>Entrance Animations</h3>
|
||||
<div class="animation-grid">
|
||||
<div class="demo-card animate-fade-in">
|
||||
<h4>Fade In</h4>
|
||||
<p>Basic fade in animation</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-fade-in-up animation-delay-200">
|
||||
<h4>Fade In Up</h4>
|
||||
<p>Fade in with upward movement</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-slide-in-up animation-delay-300">
|
||||
<h4>Slide In Up</h4>
|
||||
<p>Slide in from bottom</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-zoom-in animation-delay-500">
|
||||
<h4>Zoom In</h4>
|
||||
<p>Scale up animation</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Emphasis Animations -->
|
||||
<section class="demo-section">
|
||||
<h3>Emphasis Animations</h3>
|
||||
<div class="animation-grid">
|
||||
<button class="demo-button" (click)="animateElement('bounce')">
|
||||
Bounce
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('shake')">
|
||||
Shake
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('pulse')">
|
||||
Pulse
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('wobble')">
|
||||
Wobble
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div #animationTarget class="demo-target">
|
||||
Click buttons above to animate me!
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Directive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Animation Directive Examples</h3>
|
||||
<div class="animation-grid">
|
||||
<div class="demo-card" uiAnimate="animate-fade-in-up" [animationTrigger]="'hover'">
|
||||
<h4>Hover to Animate</h4>
|
||||
<p>Uses directive with hover trigger</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" uiAnimate="animate-bounce" [animationTrigger]="'click'" [animationOnce]="true">
|
||||
<h4>Click to Animate</h4>
|
||||
<p>Uses directive with click trigger</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Service Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Animation Service Examples</h3>
|
||||
<div class="service-controls">
|
||||
<button (click)="serviceAnimate('animate-tada')">Tada</button>
|
||||
<button (click)="serviceAnimate('animate-jello')">Jello</button>
|
||||
<button (click)="serviceAnimate('animate-heartbeat')">Heartbeat</button>
|
||||
<button (click)="serviceAnimate('animate-rubber-band')">Rubber Band</button>
|
||||
</div>
|
||||
|
||||
<div #serviceTarget class="demo-target service-target">
|
||||
Animation Service Target
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.animations-demo {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.animation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-target {
|
||||
padding: 2rem;
|
||||
border: 2px dashed #007bff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
background: #f8f9fa;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.service-target {
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.service-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.animation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.service-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class AnimationsDemoComponent {
|
||||
@ViewChild('animationTarget', { static: true }) animationTarget!: ElementRef<HTMLElement>;
|
||||
@ViewChild('serviceTarget', { static: true }) serviceTarget!: ElementRef<HTMLElement>;
|
||||
|
||||
constructor(private animationService: UiAnimationsService) {}
|
||||
|
||||
animateElement(animationType: string) {
|
||||
const element = this.animationTarget.nativeElement;
|
||||
const animationClass = `animate-${animationType}`;
|
||||
|
||||
// Remove any existing animation classes first
|
||||
element.className = element.className.replace(/animate-\w+/g, '');
|
||||
|
||||
// Add animation class
|
||||
this.animationService.animateOnce(element, animationClass);
|
||||
}
|
||||
|
||||
serviceAnimate(animationClass: string) {
|
||||
const element = this.serviceTarget.nativeElement;
|
||||
|
||||
// Remove any existing animation classes first
|
||||
element.className = element.className.replace(/animate-\w+/g, '');
|
||||
|
||||
// Add animation class with service
|
||||
this.animationService.animateOnce(element, animationClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
.backgrounds-demo {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-900, #1e293b);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--neutral-600, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 4rem;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-800, #334155);
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-200, #e2e8f0);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.example-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.pattern-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pattern-demo {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pattern-name {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.gradient-demo {
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-demo {
|
||||
padding: 3rem;
|
||||
border-radius: 1rem;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--primary-700, #475569);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.backgrounds-demo {
|
||||
padding: 1rem;
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.example-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pattern-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.interactive-demo {
|
||||
padding: 2rem;
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.backgrounds-demo {
|
||||
.demo-header h1 {
|
||||
color: var(--neutral-100, #f8fafc);
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: var(--neutral-200, #e2e8f0);
|
||||
border-bottom-color: var(--primary-700, #475569);
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
color: var(--neutral-200, #e2e8f0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast mode
|
||||
@media (prefers-contrast: high) {
|
||||
.backgrounds-demo {
|
||||
.demo-card,
|
||||
.pattern-demo,
|
||||
.gradient-demo,
|
||||
.interactive-demo {
|
||||
border: 2px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.backgrounds-demo {
|
||||
* {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
BackgroundDirective,
|
||||
SolidBackgroundComponent,
|
||||
GradientBackgroundComponent,
|
||||
PatternBackgroundComponent,
|
||||
ImageBackgroundComponent,
|
||||
BackgroundService,
|
||||
SolidBackgroundConfig,
|
||||
LinearGradientConfig,
|
||||
PatternConfig,
|
||||
ImageBackgroundConfig
|
||||
} from 'ui-backgrounds';
|
||||
|
||||
@Component({
|
||||
selector: 'app-backgrounds-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
BackgroundDirective,
|
||||
SolidBackgroundComponent,
|
||||
GradientBackgroundComponent,
|
||||
PatternBackgroundComponent,
|
||||
ImageBackgroundComponent
|
||||
],
|
||||
template: `
|
||||
<div class="backgrounds-demo">
|
||||
<div class="demo-header">
|
||||
<h1>UI Backgrounds Demo</h1>
|
||||
<p>Explore different background types and configurations</p>
|
||||
</div>
|
||||
|
||||
<!-- Directive Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Background Directive Examples</h2>
|
||||
|
||||
<div class="example-grid">
|
||||
<div class="demo-card" [uiBackground]="solidConfig()">
|
||||
<h3>Solid Background</h3>
|
||||
<p>Using the background directive with solid color</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" [uiBackground]="gradientConfig()">
|
||||
<h3>Linear Gradient</h3>
|
||||
<p>Beautiful gradient backgrounds</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" [uiBackground]="patternConfig()">
|
||||
<h3>Pattern Background</h3>
|
||||
<p>Geometric patterns</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Component Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Background Components</h2>
|
||||
|
||||
<div class="example-grid">
|
||||
<ui-solid-background color="#4ade80" class="demo-card">
|
||||
<h3>Solid Component</h3>
|
||||
<p>Green background using component</p>
|
||||
</ui-solid-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="linear"
|
||||
[colors]="['#8b5cf6', '#06b6d4']"
|
||||
direction="45deg"
|
||||
class="demo-card">
|
||||
<h3>Gradient Component</h3>
|
||||
<p>Purple to cyan gradient</p>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-pattern-background
|
||||
pattern="dots"
|
||||
primaryColor="#f59e0b"
|
||||
secondaryColor="transparent"
|
||||
[size]="30"
|
||||
[opacity]="0.7"
|
||||
class="demo-card">
|
||||
<h3>Pattern Component</h3>
|
||||
<p>Orange dots pattern</p>
|
||||
</ui-pattern-background>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pattern Showcase -->
|
||||
<section class="demo-section">
|
||||
<h2>Pattern Showcase</h2>
|
||||
|
||||
<div class="pattern-grid">
|
||||
<div
|
||||
*ngFor="let pattern of patterns"
|
||||
class="pattern-demo"
|
||||
[uiBackground]="getPatternConfig(pattern)">
|
||||
<span class="pattern-name">{{ pattern }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gradient Types -->
|
||||
<section class="demo-section">
|
||||
<h2>Gradient Types</h2>
|
||||
|
||||
<div class="gradient-grid">
|
||||
<ui-gradient-background
|
||||
type="linear"
|
||||
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1']"
|
||||
direction="to right"
|
||||
class="gradient-demo">
|
||||
<span>Linear Gradient</span>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="radial"
|
||||
[colors]="['#667eea', '#764ba2']"
|
||||
shape="circle"
|
||||
class="gradient-demo">
|
||||
<span>Radial Gradient</span>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="conic"
|
||||
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#ff6b6b']"
|
||||
class="gradient-demo">
|
||||
<span>Conic Gradient</span>
|
||||
</ui-gradient-background>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section class="demo-section">
|
||||
<h2>Interactive Background</h2>
|
||||
|
||||
<div class="interactive-demo" [uiBackground]="interactiveConfig()">
|
||||
<div class="controls">
|
||||
<button
|
||||
*ngFor="let config of presetConfigs"
|
||||
(click)="setInteractiveConfig(config)"
|
||||
class="preset-btn">
|
||||
{{ config.name }}
|
||||
</button>
|
||||
</div>
|
||||
<h3>Interactive Background Demo</h3>
|
||||
<p>Click buttons to change the background</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Full Screen Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Full Screen Backgrounds</h2>
|
||||
|
||||
<div class="fullscreen-controls">
|
||||
<button (click)="toggleFullScreenBackground()" class="action-btn">
|
||||
{{ hasFullScreenBg() ? 'Remove' : 'Add' }} Full Screen Background
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let bg of fullScreenPresets"
|
||||
(click)="setFullScreenBackground(bg)"
|
||||
class="preset-btn">
|
||||
{{ bg.name }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './backgrounds-demo.component.scss'
|
||||
})
|
||||
export class BackgroundsDemoComponent {
|
||||
private readonly backgroundService = signal(new BackgroundService());
|
||||
private fullScreenBackgroundId = signal<string | null>(null);
|
||||
|
||||
// Configuration signals
|
||||
solidConfig = signal<SolidBackgroundConfig>({
|
||||
type: 'solid',
|
||||
color: '#3b82f6'
|
||||
});
|
||||
|
||||
gradientConfig = signal<LinearGradientConfig>({
|
||||
type: 'linear-gradient',
|
||||
direction: 'to bottom right',
|
||||
colors: [
|
||||
{ color: '#ec4899' },
|
||||
{ color: '#8b5cf6' }
|
||||
]
|
||||
});
|
||||
|
||||
patternConfig = signal<PatternConfig>({
|
||||
type: 'pattern',
|
||||
pattern: 'grid',
|
||||
primaryColor: '#1f2937',
|
||||
secondaryColor: 'transparent',
|
||||
size: 20,
|
||||
opacity: 0.3
|
||||
});
|
||||
|
||||
interactiveConfig = signal(this.solidConfig());
|
||||
|
||||
// Pattern list for showcase
|
||||
patterns = [
|
||||
'dots', 'grid', 'stripes', 'diagonal-stripes',
|
||||
'chevron', 'waves', 'circles', 'checkerboard'
|
||||
];
|
||||
|
||||
// Preset configurations
|
||||
presetConfigs = [
|
||||
{
|
||||
name: 'Ocean',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to bottom',
|
||||
colors: [{ color: '#667eea' }, { color: '#764ba2' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Sunset',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to right',
|
||||
colors: [{ color: '#ff6b6b' }, { color: '#feca57' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Forest',
|
||||
config: {
|
||||
type: 'solid' as const,
|
||||
color: '#10b981'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Dots',
|
||||
config: {
|
||||
type: 'pattern' as const,
|
||||
pattern: 'dots' as const,
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: 'transparent',
|
||||
size: 25,
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Full screen presets
|
||||
fullScreenPresets = [
|
||||
{
|
||||
name: 'Purple Mesh',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to bottom right',
|
||||
colors: [{ color: '#667eea' }, { color: '#764ba2' }, { color: '#f093fb' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Geometric',
|
||||
config: {
|
||||
type: 'pattern' as const,
|
||||
pattern: 'chevron' as const,
|
||||
primaryColor: '#1f2937',
|
||||
secondaryColor: 'transparent',
|
||||
size: 40,
|
||||
opacity: 0.1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
getPatternConfig(pattern: string): PatternConfig {
|
||||
return {
|
||||
type: 'pattern',
|
||||
pattern: pattern as any,
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: 'transparent',
|
||||
size: 25,
|
||||
opacity: 0.4
|
||||
};
|
||||
}
|
||||
|
||||
setInteractiveConfig(preset: any) {
|
||||
this.interactiveConfig.set(preset.config);
|
||||
}
|
||||
|
||||
toggleFullScreenBackground() {
|
||||
const currentId = this.fullScreenBackgroundId();
|
||||
if (currentId) {
|
||||
this.backgroundService().removeBackground(currentId);
|
||||
this.fullScreenBackgroundId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
setFullScreenBackground(preset: any) {
|
||||
// Remove existing full screen background
|
||||
const currentId = this.fullScreenBackgroundId();
|
||||
if (currentId) {
|
||||
this.backgroundService().removeBackground(currentId);
|
||||
}
|
||||
|
||||
// Add new full screen background
|
||||
const id = this.backgroundService().applyFullScreenBackground(preset.config, {
|
||||
zIndex: -1
|
||||
});
|
||||
this.fullScreenBackgroundId.set(id);
|
||||
}
|
||||
|
||||
hasFullScreenBg() {
|
||||
return this.fullScreenBackgroundId() !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h1 {
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
border-bottom: 2px solid $semantic-color-border-primary;
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
font-size: $base-typography-font-size-xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-md 0;
|
||||
font-size: $base-typography-font-size-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
line-height: $base-typography-line-height-relaxed;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border: $semantic-border-button-width $semantic-border-button-style $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
font-size: $base-typography-font-size-sm;
|
||||
text-transform: capitalize;
|
||||
transition: all $semantic-duration-fast $semantic-easing-standard;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-brand-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
border-color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $semantic-border-focus-width solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CodeSnippetComponent,
|
||||
InlineCodeComponent,
|
||||
CodeBlockComponent,
|
||||
CodeThemeService,
|
||||
CodeTheme
|
||||
} from 'ui-code-display';
|
||||
|
||||
@Component({
|
||||
selector: 'app-code-display-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
CodeSnippetComponent,
|
||||
InlineCodeComponent,
|
||||
CodeBlockComponent
|
||||
],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h1>Code Display Components Demo</h1>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Theme Selector</h2>
|
||||
<div class="theme-controls">
|
||||
@for (theme of themes; track theme) {
|
||||
<button
|
||||
(click)="setTheme(theme)"
|
||||
[class.active]="currentTheme() === theme"
|
||||
class="theme-btn">
|
||||
{{ theme }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Inline Code</h2>
|
||||
<p>
|
||||
Use <ui-inline-code code="console.log('hello')" language="javascript"></ui-inline-code>
|
||||
to output to the console.
|
||||
</p>
|
||||
<p>
|
||||
Install dependencies with <ui-inline-code code="npm install" language="bash" [copyable]="true"></ui-inline-code>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Code Snippet</h2>
|
||||
<ui-code-snippet
|
||||
[code]="typescriptCode"
|
||||
language="typescript"
|
||||
title="Component Example"
|
||||
size="md"
|
||||
[showLineNumbers]="true"
|
||||
[copyable]="true"
|
||||
[highlightLines]="[3, 5, '7-9']">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Code Block</h2>
|
||||
<ui-code-block
|
||||
[code]="cssCode"
|
||||
language="css"
|
||||
variant="bordered"
|
||||
[showLineNumbers]="true"
|
||||
[copyable]="true">
|
||||
</ui-code-block>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Different Sizes</h2>
|
||||
<h3>Small</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="sm"
|
||||
title="Small Size">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>Medium (Default)</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="md"
|
||||
title="Medium Size">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>Large</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="lg"
|
||||
title="Large Size">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Different Languages</h2>
|
||||
|
||||
<h3>Python</h3>
|
||||
<ui-code-snippet
|
||||
[code]="pythonCode"
|
||||
language="python"
|
||||
title="Python Example">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>JSON</h3>
|
||||
<ui-code-snippet
|
||||
[code]="jsonCode"
|
||||
language="json"
|
||||
title="Configuration">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>HTML</h3>
|
||||
<ui-code-snippet
|
||||
[code]="htmlCode"
|
||||
language="html"
|
||||
title="Template">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './code-display-demo.component.scss'
|
||||
})
|
||||
export class CodeDisplayDemoComponent {
|
||||
|
||||
constructor(private themeService: CodeThemeService) {
|
||||
this.themes = this.themeService.getAvailableThemes();
|
||||
this.currentTheme = this.themeService.theme;
|
||||
}
|
||||
|
||||
readonly themes: any[];
|
||||
readonly currentTheme: any;
|
||||
|
||||
readonly typescriptCode = `import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
template: \`
|
||||
<div class="container">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
\`
|
||||
})
|
||||
export class ExampleComponent {
|
||||
title = 'Hello World';
|
||||
description = 'This is a demo component';
|
||||
}`;
|
||||
|
||||
readonly cssCode = `.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}`;
|
||||
|
||||
readonly shortCode = `const greeting = 'Hello, World!';
|
||||
console.log(greeting);`;
|
||||
|
||||
readonly pythonCode = `def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
|
||||
# Generate first 10 Fibonacci numbers
|
||||
for i in range(10):
|
||||
print(f"F({i}) = {fibonacci(i)}")`;
|
||||
|
||||
readonly jsonCode = `{
|
||||
"name": "ui-code-display",
|
||||
"version": "1.0.0",
|
||||
"description": "Code display components for Angular",
|
||||
"dependencies": {
|
||||
"@angular/core": "^19.0.0",
|
||||
"prismjs": "^1.30.0"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"code",
|
||||
"syntax-highlighting",
|
||||
"prism"
|
||||
]
|
||||
}`;
|
||||
|
||||
readonly htmlCode = `<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Card Title</h2>
|
||||
<button class="close-btn">×</button>
|
||||
</header>
|
||||
|
||||
<div class="card-body">
|
||||
<p>This is the card content.</p>
|
||||
<a href="#" class="btn btn-primary">Action</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTheme(theme: CodeTheme): void {
|
||||
this.themeService.setTheme(theme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
// Import all utilities from ui-data-utils
|
||||
import {
|
||||
// Types
|
||||
SortConfig, FilterConfig, PaginationConfig,
|
||||
// Sorting
|
||||
sortBy, sortByMultiple, createComparator,
|
||||
// Filtering
|
||||
filterBy, filterByMultiple, searchFilter,
|
||||
// Pagination
|
||||
paginate, calculatePages, getPaginationRange,
|
||||
// Transformation
|
||||
groupBy, aggregate, pluck, flatten, pivot, unique, frequency
|
||||
} from 'ui-data-utils';
|
||||
|
||||
interface SampleData {
|
||||
id: number;
|
||||
name: string;
|
||||
department: string;
|
||||
salary: number;
|
||||
hireDate: string;
|
||||
active: boolean;
|
||||
skills: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-data-utils-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>UI Data Utils Demo</h2>
|
||||
<p>Comprehensive demonstration of data manipulation utilities</p>
|
||||
|
||||
<!-- Sample Data Display -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Sample Employee Data ({{ sampleData.length }} records)</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
||||
<h4>Raw Data Preview:</h4>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ getSampleDataPreview() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sorting Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Sorting Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Single Sort -->
|
||||
<div>
|
||||
<h4>Single Property Sort</h4>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Sort by: </label>
|
||||
<select [(ngModel)]="sortField" (ngModelChange)="applySingleSort()">
|
||||
<option value="name">Name</option>
|
||||
<option value="department">Department</option>
|
||||
<option value="salary">Salary</option>
|
||||
<option value="hireDate">Hire Date</option>
|
||||
</select>
|
||||
<label style="margin-left: 1rem;">
|
||||
<input type="checkbox" [(ngModel)]="sortDesc" (ngModelChange)="applySingleSort()">
|
||||
Descending
|
||||
</label>
|
||||
</div>
|
||||
<div style="background: #e3f2fd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getSortedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-column Sort -->
|
||||
<div>
|
||||
<h4>Multi-column Sort</h4>
|
||||
<p style="font-size: 0.9em; color: #666;">Department → Salary → Name</p>
|
||||
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getMultiSortedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filtering Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔍 Filtering Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Search Filter -->
|
||||
<div>
|
||||
<h4>Search Filter</h4>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search in name, department..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="applySearchFilter()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div style="background: #fff3cd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getSearchFilteredDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Property Filter -->
|
||||
<div>
|
||||
<h4>Property Filters</h4>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Department: </label>
|
||||
<select [(ngModel)]="filterDepartment" (ngModelChange)="applyPropertyFilter()">
|
||||
<option value="">All Departments</option>
|
||||
<option value="Engineering">Engineering</option>
|
||||
<option value="Marketing">Marketing</option>
|
||||
<option value="Sales">Sales</option>
|
||||
<option value="HR">HR</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Min Salary: </label>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="minSalary"
|
||||
(ngModelChange)="applyPropertyFilter()"
|
||||
placeholder="50000"
|
||||
style="width: 120px; padding: 0.25rem;"
|
||||
>
|
||||
</div>
|
||||
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPropertyFilteredDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pagination Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📄 Pagination Utilities</h3>
|
||||
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<label>Page Size: </label>
|
||||
<select [(ngModel)]="pageSize" (ngModelChange)="updatePagination()">
|
||||
<option [value]="3">3</option>
|
||||
<option [value]="5">5</option>
|
||||
<option [value]="7">7</option>
|
||||
<option [value]="10">10</option>
|
||||
</select>
|
||||
|
||||
<label>Page: </label>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="currentPage"
|
||||
(ngModelChange)="updatePagination()"
|
||||
[min]="1"
|
||||
[max]="paginationResult().totalPages"
|
||||
style="width: 60px; padding: 0.25rem;"
|
||||
>
|
||||
|
||||
<span>of {{ paginationResult().totalPages }}</span>
|
||||
|
||||
<button
|
||||
(click)="previousPage()"
|
||||
[disabled]="!paginationResult().hasPrevious"
|
||||
style="padding: 0.25rem 0.5rem;"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
(click)="nextPage()"
|
||||
[disabled]="!paginationResult().hasNext"
|
||||
style="padding: 0.25rem 0.5rem;"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<strong>Page Navigation: </strong>
|
||||
@for (page of pageRange(); track page) {
|
||||
@if (page === 'ellipsis') {
|
||||
<span style="margin: 0 0.5rem;">...</span>
|
||||
} @else {
|
||||
<button
|
||||
(click)="goToPage(page)"
|
||||
[style.font-weight]="page === currentPage() ? 'bold' : 'normal'"
|
||||
[style.background]="page === currentPage() ? '#007bff' : '#f8f9fa'"
|
||||
[style.color]="page === currentPage() ? 'white' : 'black'"
|
||||
style="padding: 0.25rem 0.5rem; margin: 0 0.125rem; border: 1px solid #ccc; cursor: pointer;"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="background: #e1f5fe; padding: 1rem; border-radius: 4px;">
|
||||
<p><strong>Showing:</strong> {{ getItemRangeText() }}</p>
|
||||
<pre style="max-height: 150px; overflow-y: auto;">{{ getPaginatedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transformation Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Transformation Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Group By -->
|
||||
<div>
|
||||
<h4>Group By Department</h4>
|
||||
<div style="background: #f1f8e9; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getGroupedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aggregation -->
|
||||
<div>
|
||||
<h4>Salary Statistics by Department</h4>
|
||||
<div style="background: #fce4ec; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getAggregationPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pluck -->
|
||||
<div>
|
||||
<h4>Extract Names & Salaries</h4>
|
||||
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPluckedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pivot Table -->
|
||||
<div>
|
||||
<h4>Pivot: Department vs Active Status</h4>
|
||||
<div style="background: #fff3e0; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPivotDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Combined Operations Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Combined Operations</h3>
|
||||
<p>Demonstrate chaining multiple operations: Filter → Sort → Paginate</p>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
|
||||
<label>
|
||||
Filter Active:
|
||||
<select [(ngModel)]="combinedActiveFilter" (ngModelChange)="updateCombinedDemo()">
|
||||
<option value="">All</option>
|
||||
<option value="true">Active Only</option>
|
||||
<option value="false">Inactive Only</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Sort By:
|
||||
<select [(ngModel)]="combinedSortField" (ngModelChange)="updateCombinedDemo()">
|
||||
<option value="name">Name</option>
|
||||
<option value="salary">Salary</option>
|
||||
<option value="hireDate">Hire Date</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="combinedSortDesc" (ngModelChange)="updateCombinedDemo()">
|
||||
Descending
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px;">
|
||||
<p><strong>Pipeline:</strong>
|
||||
{{ sampleData.length }} records →
|
||||
{{ combinedFiltered().length }} filtered →
|
||||
{{ combinedSorted().length }} sorted →
|
||||
{{ combinedPaginated().data.length }} on page {{ combinedPaginated().page }}
|
||||
</p>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ getCombinedResultPreview() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Metrics -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📊 Performance & Statistics</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Total Records</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #007bff;">{{ sampleData.length }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Departments</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #28a745;">{{ getDepartmentCount() }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Avg Salary</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #ffc107;">{{ getAverageSalary() }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Active Employees</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #17a2b8;">{{ getActiveCount() }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>💻 Code Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px;">
|
||||
|
||||
<h4>Sorting Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Single sort
|
||||
const sorted = sortBy(data, {{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }});
|
||||
|
||||
// Multi-column sort
|
||||
const multiSorted = sortByMultiple(data, {{ '{' }}
|
||||
sorts: [
|
||||
{{ '{' }} key: 'department', direction: 'asc', dataType: 'string' {{ '}' }},
|
||||
{{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }}
|
||||
]
|
||||
{{ '}' }});</code></pre>
|
||||
|
||||
<h4>Filtering Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Property filter
|
||||
const filtered = filterBy(data, {{ '{' }}
|
||||
key: 'salary',
|
||||
operator: 'greater_than',
|
||||
value: 50000,
|
||||
dataType: 'number'
|
||||
{{ '}' }});
|
||||
|
||||
// Search across multiple properties
|
||||
const searched = searchFilter(data, 'engineer', ['name', 'department']);</code></pre>
|
||||
|
||||
<h4>Pagination Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Paginate data
|
||||
const paginatedResult = paginate(data, {{ '{' }} page: 1, pageSize: 10 {{ '}' }});
|
||||
|
||||
// Get page range for UI
|
||||
const range = getPaginationRange(currentPage, totalPages, 7);</code></pre>
|
||||
|
||||
<h4>Transformation Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Group by property
|
||||
const groups = groupBy(data, 'department');
|
||||
|
||||
// Aggregate values
|
||||
const stats = aggregate(data, 'salary', ['sum', 'avg', 'min', 'max']);
|
||||
|
||||
// Extract specific properties
|
||||
const names = pluck(data, ['name', 'salary']);</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Library Info -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📚 Library Information</h3>
|
||||
<div style="background: #e3f2fd; padding: 1.5rem; border-radius: 4px;">
|
||||
<h4>UI Data Utils Features:</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
||||
<div>
|
||||
<h5>🔄 Sorting</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Single & multi-column sorting</li>
|
||||
<li>Type-aware comparisons</li>
|
||||
<li>Stable sort algorithms</li>
|
||||
<li>Custom comparator functions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>🔍 Filtering</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Multiple filter operators</li>
|
||||
<li>Search across properties</li>
|
||||
<li>Complex filter combinations</li>
|
||||
<li>Type-specific filtering</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>📄 Pagination</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Client-side pagination</li>
|
||||
<li>Page range calculations</li>
|
||||
<li>Pagination metadata</li>
|
||||
<li>Navigation utilities</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>🔄 Transformation</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Group by operations</li>
|
||||
<li>Data aggregations</li>
|
||||
<li>Property extraction</li>
|
||||
<li>Pivot tables</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(279, 14%, 35%);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: hsl(279, 14%, 45%);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
code {
|
||||
color: hsl(279, 14%, 15%);
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input, select {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class DataUtilsDemoComponent {
|
||||
// Sample data
|
||||
sampleData: SampleData[] = [
|
||||
{ id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, hireDate: '2022-01-15', active: true, skills: ['JavaScript', 'Angular', 'TypeScript'] },
|
||||
{ id: 2, name: 'Bob Smith', department: 'Marketing', salary: 65000, hireDate: '2021-03-22', active: true, skills: ['SEO', 'Content Marketing'] },
|
||||
{ id: 3, name: 'Charlie Brown', department: 'Engineering', salary: 87000, hireDate: '2023-02-10', active: false, skills: ['React', 'Node.js'] },
|
||||
{ id: 4, name: 'Diana Davis', department: 'Sales', salary: 72000, hireDate: '2020-11-05', active: true, skills: ['CRM', 'Negotiation'] },
|
||||
{ id: 5, name: 'Eve Wilson', department: 'Engineering', salary: 102000, hireDate: '2021-08-18', active: true, skills: ['Python', 'Machine Learning'] },
|
||||
{ id: 6, name: 'Frank Miller', department: 'HR', salary: 58000, hireDate: '2022-05-30', active: false, skills: ['Recruiting', 'Employee Relations'] },
|
||||
{ id: 7, name: 'Grace Lee', department: 'Marketing', salary: 69000, hireDate: '2023-01-12', active: true, skills: ['Brand Management', 'Social Media'] },
|
||||
{ id: 8, name: 'Henry Taylor', department: 'Sales', salary: 78000, hireDate: '2021-12-03', active: true, skills: ['Sales Strategy', 'Lead Generation'] },
|
||||
{ id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 91000, hireDate: '2022-09-14', active: true, skills: ['Vue.js', 'GraphQL'] },
|
||||
{ id: 10, name: 'Jack White', department: 'HR', salary: 62000, hireDate: '2020-04-25', active: false, skills: ['Payroll', 'Benefits Administration'] }
|
||||
];
|
||||
|
||||
// Sorting controls
|
||||
sortField = signal<keyof SampleData>('name');
|
||||
sortDesc = signal<boolean>(false);
|
||||
|
||||
// Filtering controls
|
||||
searchTerm = signal<string>('');
|
||||
filterDepartment = signal<string>('');
|
||||
minSalary = signal<number | null>(null);
|
||||
|
||||
// Pagination controls
|
||||
currentPage = signal<number>(1);
|
||||
pageSize = signal<number>(5);
|
||||
|
||||
// Combined demo controls
|
||||
combinedActiveFilter = signal<string>('');
|
||||
combinedSortField = signal<keyof SampleData>('name');
|
||||
combinedSortDesc = signal<boolean>(false);
|
||||
|
||||
// Computed data
|
||||
sortedData = computed(() => {
|
||||
const config: SortConfig<SampleData> = {
|
||||
key: this.sortField(),
|
||||
direction: this.sortDesc() ? 'desc' : 'asc',
|
||||
dataType: this.getDataType(this.sortField())
|
||||
};
|
||||
return sortBy(this.sampleData, config);
|
||||
});
|
||||
|
||||
multiSortedData = computed(() => {
|
||||
return sortByMultiple(this.sampleData, {
|
||||
sorts: [
|
||||
{ key: 'department', direction: 'asc', dataType: 'string' },
|
||||
{ key: 'salary', direction: 'desc', dataType: 'number' },
|
||||
{ key: 'name', direction: 'asc', dataType: 'string' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
searchFilteredData = computed(() => {
|
||||
if (!this.searchTerm()) return this.sampleData;
|
||||
return searchFilter(this.sampleData, this.searchTerm(), ['name', 'department']);
|
||||
});
|
||||
|
||||
propertyFilteredData = computed(() => {
|
||||
let filtered = this.sampleData;
|
||||
|
||||
const filters: FilterConfig<SampleData>[] = [];
|
||||
|
||||
if (this.filterDepartment()) {
|
||||
filters.push({
|
||||
key: 'department',
|
||||
operator: 'equals',
|
||||
value: this.filterDepartment(),
|
||||
dataType: 'string'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.minSalary() !== null) {
|
||||
filters.push({
|
||||
key: 'salary',
|
||||
operator: 'greater_than_equal',
|
||||
value: this.minSalary(),
|
||||
dataType: 'number'
|
||||
});
|
||||
}
|
||||
|
||||
return filters.length > 0 ? filterByMultiple(filtered, filters) : filtered;
|
||||
});
|
||||
|
||||
paginationResult = computed(() => {
|
||||
return paginate(this.sampleData, {
|
||||
page: this.currentPage(),
|
||||
pageSize: this.pageSize()
|
||||
});
|
||||
});
|
||||
|
||||
pageRange = computed(() => {
|
||||
return getPaginationRange(this.currentPage(), this.paginationResult().totalPages, 7);
|
||||
});
|
||||
|
||||
// Combined operations
|
||||
combinedFiltered = computed(() => {
|
||||
if (!this.combinedActiveFilter()) return this.sampleData;
|
||||
return filterBy(this.sampleData, {
|
||||
key: 'active',
|
||||
operator: 'equals',
|
||||
value: this.combinedActiveFilter() === 'true',
|
||||
dataType: 'boolean'
|
||||
});
|
||||
});
|
||||
|
||||
combinedSorted = computed(() => {
|
||||
return sortBy(this.combinedFiltered(), {
|
||||
key: this.combinedSortField(),
|
||||
direction: this.combinedSortDesc() ? 'desc' : 'asc',
|
||||
dataType: this.getDataType(this.combinedSortField())
|
||||
});
|
||||
});
|
||||
|
||||
combinedPaginated = computed(() => {
|
||||
return paginate(this.combinedSorted(), { page: 1, pageSize: 5 });
|
||||
});
|
||||
|
||||
// Helper methods
|
||||
private getDataType(key: keyof SampleData): 'string' | 'number' | 'date' | 'boolean' {
|
||||
switch (key) {
|
||||
case 'salary': case 'id': return 'number';
|
||||
case 'hireDate': return 'date';
|
||||
case 'active': return 'boolean';
|
||||
default: return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
getSampleDataPreview(): string {
|
||||
return JSON.stringify(this.sampleData.slice(0, 3), null, 2);
|
||||
}
|
||||
|
||||
getSortedDataPreview(): string {
|
||||
return JSON.stringify(this.sortedData().slice(0, 3).map(item => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
|
||||
}
|
||||
|
||||
getMultiSortedDataPreview(): string {
|
||||
return JSON.stringify(this.multiSortedData().slice(0, 4).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getSearchFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.searchFilteredData().slice(0, 3).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPropertyFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPaginatedDataPreview(): string {
|
||||
return JSON.stringify(this.paginationResult().data.map(item => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getGroupedDataPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
return JSON.stringify(grouped.map(group => ({
|
||||
department: group.key,
|
||||
count: group.count,
|
||||
employees: group.items.map(emp => emp.name)
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getAggregationPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
const result = grouped.map(group => ({
|
||||
department: group.key,
|
||||
...aggregate(group.items, 'salary', ['sum', 'avg', 'min', 'max', 'count'])
|
||||
}));
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
getPluckedDataPreview(): string {
|
||||
const plucked = pluck(this.sampleData, ['name', 'salary']);
|
||||
return JSON.stringify(plucked.slice(0, 5), null, 2);
|
||||
}
|
||||
|
||||
getPivotDataPreview(): string {
|
||||
const pivotData = this.sampleData.map(emp => ({
|
||||
department: emp.department,
|
||||
status: emp.active ? 'Active' : 'Inactive',
|
||||
count: 1
|
||||
}));
|
||||
|
||||
const pivotResult = pivot(pivotData, 'department', 'status', 'count', 'sum');
|
||||
return JSON.stringify(pivotResult, null, 2);
|
||||
}
|
||||
|
||||
getCombinedResultPreview(): string {
|
||||
return JSON.stringify(this.combinedPaginated().data.map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary,
|
||||
active: item.active
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getItemRangeText(): string {
|
||||
const result = this.paginationResult();
|
||||
if (result.totalItems === 0) return '0 of 0';
|
||||
return `${result.startIndex + 1}-${result.endIndex + 1} of ${result.totalItems}`;
|
||||
}
|
||||
|
||||
getDepartmentCount(): number {
|
||||
return unique(this.sampleData, 'department').length;
|
||||
}
|
||||
|
||||
getAverageSalary(): string {
|
||||
const avg = aggregate(this.sampleData, 'salary', ['avg']);
|
||||
return '$' + Math.round(avg['avg'] || 0).toLocaleString();
|
||||
}
|
||||
|
||||
getActiveCount(): number {
|
||||
const activeCount = this.sampleData.filter(emp => emp.active).length;
|
||||
return Math.round((activeCount / this.sampleData.length) * 100);
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
applySingleSort(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
applySearchFilter(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
applyPropertyFilter(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
updatePagination(): void {
|
||||
// Ensure page is within bounds
|
||||
const maxPage = this.paginationResult().totalPages;
|
||||
if (this.currentPage() > maxPage) {
|
||||
this.currentPage.set(maxPage || 1);
|
||||
}
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.paginationResult().hasPrevious) {
|
||||
this.currentPage.update(page => page - 1);
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.paginationResult().hasNext) {
|
||||
this.currentPage.update(page => page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
goToPage(page: number | string): void {
|
||||
if (typeof page === 'number') {
|
||||
this.currentPage.set(page);
|
||||
}
|
||||
}
|
||||
|
||||
updateCombinedDemo(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
@@ -77,6 +78,20 @@ import { GridContainerDemoComponent } from './grid-container-demo/grid-container
|
||||
import { FeedLayoutDemoComponent } from './feed-layout-demo/feed-layout-demo.component';
|
||||
import { ListDetailLayoutDemoComponent } from './list-detail-layout-demo/list-detail-layout-demo.component';
|
||||
import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo/supporting-pane-layout-demo.component';
|
||||
import { MasonryDemoComponent } from './masonry-demo/masonry-demo.component';
|
||||
import { InfiniteScrollContainerDemoComponent } from './infinite-scroll-container-demo/infinite-scroll-container-demo.component';
|
||||
import { StickyLayoutDemoComponent } from './sticky-layout-demo/sticky-layout-demo.component';
|
||||
import { SplitViewDemoComponent } from './split-view-demo/split-view-demo.component';
|
||||
import { GalleryGridDemoComponent } from './gallery-grid-demo/gallery-grid-demo.component';
|
||||
import { AnimationsDemoComponent } from './animations-demo/animations-demo.component';
|
||||
import { AccessibilityDemoComponent } from './accessibility-demo/accessibility-demo.component';
|
||||
import { SelectDemoComponent } from './select-demo/select-demo.component';
|
||||
import { TextareaDemoComponent } from './textarea-demo/textarea-demo.component';
|
||||
import { DataUtilsDemoComponent } from './data-utils-demo/data-utils-demo.component';
|
||||
import { HclStudioDemoComponent } from './hcl-studio-demo/hcl-studio-demo.component';
|
||||
import { FontManagerDemoComponent } from './font-manager-demo/font-manager-demo.component';
|
||||
import { CodeDisplayDemoComponent } from './code-display-demo/code-display-demo.component';
|
||||
import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.component';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -128,6 +143,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-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>
|
||||
}
|
||||
@@ -140,6 +159,14 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
|
||||
<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>
|
||||
}
|
||||
@@ -360,6 +387,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
|
||||
<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>
|
||||
}
|
||||
@@ -384,12 +415,56 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
|
||||
<ui-supporting-pane-layout-demo></ui-supporting-pane-layout-demo>
|
||||
}
|
||||
|
||||
@case ("masonry") {
|
||||
<ui-masonry-demo></ui-masonry-demo>
|
||||
}
|
||||
|
||||
@case ("sticky-layout") {
|
||||
<ui-sticky-layout-demo></ui-sticky-layout-demo>
|
||||
}
|
||||
|
||||
@case ("split-view") {
|
||||
<ui-split-view-demo></ui-split-view-demo>
|
||||
}
|
||||
|
||||
@case ("gallery-grid") {
|
||||
<ui-gallery-grid-demo></ui-gallery-grid-demo>
|
||||
}
|
||||
|
||||
@case ("animations") {
|
||||
<app-animations-demo></app-animations-demo>
|
||||
}
|
||||
|
||||
@case ("accessibility") {
|
||||
<app-accessibility-demo></app-accessibility-demo>
|
||||
}
|
||||
|
||||
@case ("data-utils") {
|
||||
<ui-data-utils-demo></ui-data-utils-demo>
|
||||
}
|
||||
|
||||
@case ("hcl-studio") {
|
||||
<app-hcl-studio-demo></app-hcl-studio-demo>
|
||||
}
|
||||
|
||||
@case ("font-manager") {
|
||||
<app-font-manager-demo></app-font-manager-demo>
|
||||
}
|
||||
|
||||
@case ("code-display") {
|
||||
<app-code-display-demo></app-code-display-demo>
|
||||
}
|
||||
|
||||
@case ("backgrounds") {
|
||||
<app-backgrounds-demo></app-backgrounds-demo>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
|
||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent, KanbanBoardDemoComponent,
|
||||
RadioDemoComponent, CheckboxDemoComponent,
|
||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||
@@ -399,7 +474,7 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
|
||||
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, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent]
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, InfiniteScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent, MasonryDemoComponent, StickyLayoutDemoComponent, SplitViewDemoComponent, GalleryGridDemoComponent, AnimationsDemoComponent, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent, FontManagerDemoComponent, CodeDisplayDemoComponent, BackgroundsDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<div class="demo-container">
|
||||
<h1>UI Font Manager Demo</h1>
|
||||
<p class="demo-description">
|
||||
Dynamically load Google Fonts and switch between font themes with smooth transitions.
|
||||
Select a font preset to apply it globally to the entire application.
|
||||
</p>
|
||||
|
||||
<!-- Font Preset Selection -->
|
||||
<section class="demo-section">
|
||||
<h2>Font Preset Selection</h2>
|
||||
<p class="preset-description">
|
||||
Choose from curated font combinations. The selected preset will be applied to the entire application.
|
||||
</p>
|
||||
|
||||
<div class="preset-controls">
|
||||
<div class="preset-selector">
|
||||
<label for="preset-select">Choose Font Preset:</label>
|
||||
<select
|
||||
id="preset-select"
|
||||
[value]="selectedPreset"
|
||||
(change)="onPresetChange($event)">
|
||||
<option value="">Select a preset...</option>
|
||||
<option *ngFor="let preset of fontPresets" [value]="preset.key">
|
||||
{{preset.name}} - {{preset.description}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="transition-selector" *ngIf="selectedPreset">
|
||||
<label for="transition-select">Transition Effect:</label>
|
||||
<select id="transition-select" [(ngModel)]="selectedTransition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="typewriter">Typewriter</option>
|
||||
</select>
|
||||
<button
|
||||
class="apply-btn"
|
||||
(click)="applyPresetWithTransition()">
|
||||
Apply with {{selectedTransition | titlecase}} Effect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Preset Display -->
|
||||
<div class="current-preset" *ngIf="currentPreset">
|
||||
<h4>Currently Applied: {{currentPreset.name}}</h4>
|
||||
<div class="preset-fonts">
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Sans-serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Monospace:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Display:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography Preview -->
|
||||
<section class="demo-section">
|
||||
<h2>Typography Preview</h2>
|
||||
<p class="preview-description">See how the selected fonts look in a real application context.</p>
|
||||
|
||||
<div class="typography-showcase">
|
||||
<div class="type-sample">
|
||||
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
|
||||
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
|
||||
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
|
||||
|
||||
<p style="font-family: var(--font-family-sans)">
|
||||
This is a paragraph using the sans-serif font. It demonstrates how readable body text
|
||||
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
|
||||
<blockquote style="font-family: var(--font-family-serif)">
|
||||
"This is a quote using the serif font, which often provides better readability
|
||||
for longer text passages and adds elegance to quoted content."
|
||||
</blockquote>
|
||||
|
||||
<pre><code style="font-family: var(--font-family-mono)">// Code example with monospace font
|
||||
function applyFontTheme(theme) {
|
||||
document.documentElement.style.setProperty('--font-family-sans', theme.sans);
|
||||
document.documentElement.style.setProperty('--font-family-serif', theme.serif);
|
||||
document.documentElement.style.setProperty('--font-family-mono', theme.mono);
|
||||
}</code></pre>
|
||||
|
||||
<div class="font-info">
|
||||
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
|
||||
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Font Presets -->
|
||||
<section class="demo-section">
|
||||
<h2>Available Font Presets</h2>
|
||||
<div class="themes-grid">
|
||||
<div
|
||||
*ngFor="let preset of fontPresets"
|
||||
class="theme-card"
|
||||
[class.active]="preset.key === selectedPreset">
|
||||
|
||||
<h4>{{preset.name}}</h4>
|
||||
<p class="theme-description">{{preset.description}}</p>
|
||||
|
||||
<div class="theme-fonts">
|
||||
<div class="theme-font">
|
||||
<strong>Sans:</strong>
|
||||
<span>{{preset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Serif:</strong>
|
||||
<span>{{preset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Mono:</strong>
|
||||
<span>{{preset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Display:</strong>
|
||||
<span>{{preset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="apply-theme-btn"
|
||||
(click)="applyPreset(preset.key)">
|
||||
Apply {{preset.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Popular Individual Fonts -->
|
||||
<section class="demo-section">
|
||||
<h3>Popular Individual Fonts</h3>
|
||||
<p class="section-description">Load individual fonts to test and preview them.</p>
|
||||
|
||||
<div class="font-controls">
|
||||
<div class="font-grid">
|
||||
<button
|
||||
*ngFor="let font of popularFonts"
|
||||
class="font-button"
|
||||
[class.loaded]="isLoaded(font)"
|
||||
(click)="loadSingleFont(font)">
|
||||
|
||||
{{font}}
|
||||
<span class="load-status" *ngIf="isLoaded(font)">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section" *ngIf="eventLog.length > 0">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div
|
||||
*ngFor="let event of eventLog.slice(-10)"
|
||||
class="event-item"
|
||||
[class]="event.type">
|
||||
<span class="event-time">{{event.time}}</span>
|
||||
<span class="event-message">{{event.message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,580 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 3rem 0;
|
||||
padding: 2rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
background: #fafafa;
|
||||
|
||||
h2, h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading State Display
|
||||
.state-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.state-item {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2196F3;
|
||||
|
||||
strong {
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #FF9800;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.idle {
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Font Controls
|
||||
.font-controls {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.font-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.font-button {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:hover {
|
||||
border-color: #2196F3;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
border-color: #4CAF50;
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.load-status {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
color: #4CAF50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Transition Controls
|
||||
.transition-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.transition-btn {
|
||||
padding: 1rem;
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.scale {
|
||||
border-color: #FF9800;
|
||||
color: #FF9800;
|
||||
|
||||
&:hover {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.typewriter {
|
||||
border-color: #9C27B0;
|
||||
color: #9C27B0;
|
||||
|
||||
&:hover {
|
||||
background: #9C27B0;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Typography Showcase
|
||||
.typography-showcase {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.type-sample {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
|
||||
code {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-info {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
// Available Themes
|
||||
.themes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #2196F3;
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-description {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.theme-fonts {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-font {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.85rem;
|
||||
|
||||
strong {
|
||||
color: #333;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
& + .theme-font {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-theme-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #2196F3;
|
||||
border-radius: 6px;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #1976D2;
|
||||
border-color: #1976D2;
|
||||
}
|
||||
}
|
||||
|
||||
// Event Log
|
||||
.event-log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: #2196F3;
|
||||
}
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
min-width: 80px;
|
||||
font-family: var(--font-family-mono, 'Courier New', monospace);
|
||||
}
|
||||
|
||||
.event-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Font Preset Controls
|
||||
.preset-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.preset-selector {
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #2196F3;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transition-selector {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1976D2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Current Preset Display
|
||||
.current-preset {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 12px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #2e7d32;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-fonts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.font-assignment {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
.font-type {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.font-name {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme card active state
|
||||
.theme-card.active {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8e9;
|
||||
|
||||
h4 {
|
||||
color: #2e7d32;
|
||||
}
|
||||
}
|
||||
|
||||
// Section descriptions
|
||||
.preset-description,
|
||||
.preview-description,
|
||||
.section-description {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// Font transition animations
|
||||
:global(html.font-transition-fade) {
|
||||
* {
|
||||
transition: font-family 0.8s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.font-transition-scale) {
|
||||
* {
|
||||
transition: font-family 0.6s ease-in-out, transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
body {
|
||||
transform: scale(1.02);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&.font-transition-scale body {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.font-transition-typewriter) {
|
||||
* {
|
||||
transition: font-family 0.5s steps(10, end);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced blockquote styling
|
||||
blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-left: 4px solid #2196F3;
|
||||
background: #f8f9fa;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '"';
|
||||
font-size: 3rem;
|
||||
color: #2196F3;
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.font-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.transition-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.themes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.state-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.type-sample {
|
||||
padding: 1.5rem;
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
h4 { font-size: 1.25rem; }
|
||||
}
|
||||
|
||||
.preset-controls {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.transition-selector {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
|
||||
select,
|
||||
.apply-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-fonts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
// Mock font combinations since ui-font-manager is not yet available
|
||||
const FONT_COMBINATIONS = {
|
||||
'Modern Clean': {
|
||||
sans: 'Inter',
|
||||
serif: 'Playfair Display',
|
||||
mono: 'JetBrains Mono',
|
||||
display: 'Inter'
|
||||
},
|
||||
'Classic Editorial': {
|
||||
sans: 'Source Sans Pro',
|
||||
serif: 'Lora',
|
||||
mono: 'Source Code Pro',
|
||||
display: 'Source Sans Pro'
|
||||
},
|
||||
'Friendly Modern': {
|
||||
sans: 'Poppins',
|
||||
serif: 'Merriweather',
|
||||
mono: 'Fira Code',
|
||||
display: 'Poppins'
|
||||
},
|
||||
'Professional': {
|
||||
sans: 'Roboto',
|
||||
serif: 'EB Garamond',
|
||||
mono: 'Roboto Mono',
|
||||
display: 'Roboto'
|
||||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-font-manager-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h1>UI Font Manager Demo</h1>
|
||||
<p class="demo-description">
|
||||
Dynamically load Google Fonts and switch between font themes with smooth transitions.
|
||||
Select a font preset to apply it globally to the entire application.
|
||||
</p>
|
||||
|
||||
<!-- Font Preset Selection -->
|
||||
<section class="demo-section">
|
||||
<h2>Font Preset Selection</h2>
|
||||
<p class="preset-description">
|
||||
Choose from curated font combinations. The selected preset will be applied to the entire application.
|
||||
</p>
|
||||
|
||||
<div class="preset-controls">
|
||||
<div class="preset-selector">
|
||||
<label for="preset-select">Choose Font Preset:</label>
|
||||
<select
|
||||
id="preset-select"
|
||||
[value]="selectedPreset"
|
||||
(change)="onPresetChange($event)">
|
||||
<option value="">Select a preset...</option>
|
||||
<option *ngFor="let preset of fontPresets" [value]="preset.key">
|
||||
{{preset.name}} - {{preset.description}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="transition-selector" *ngIf="selectedPreset">
|
||||
<label for="transition-select">Transition Effect:</label>
|
||||
<select id="transition-select" [(ngModel)]="selectedTransition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="typewriter">Typewriter</option>
|
||||
</select>
|
||||
<button
|
||||
class="apply-btn"
|
||||
(click)="applyPresetWithTransition()">
|
||||
Apply with {{selectedTransition | titlecase}} Effect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Preset Display -->
|
||||
<div class="current-preset" *ngIf="currentPreset">
|
||||
<h4>Currently Applied: {{currentPreset.name}}</h4>
|
||||
<div class="preset-fonts">
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Sans-serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Monospace:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Display:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography Preview -->
|
||||
<section class="demo-section">
|
||||
<h2>Typography Preview</h2>
|
||||
<p class="preview-description">See how the selected fonts look in a real application context.</p>
|
||||
|
||||
<div class="typography-showcase">
|
||||
<div class="type-sample">
|
||||
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
|
||||
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
|
||||
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
|
||||
|
||||
<p style="font-family: var(--font-family-sans)">
|
||||
This is a paragraph using the sans-serif font. It demonstrates how readable body text
|
||||
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
|
||||
<blockquote style="font-family: var(--font-family-serif)">
|
||||
"This is a quote using the serif font, which often provides better readability
|
||||
for longer text passages and adds elegance to quoted content."
|
||||
</blockquote>
|
||||
|
||||
<div class="font-info">
|
||||
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
|
||||
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Font Presets -->
|
||||
<section class="demo-section">
|
||||
<h2>Available Font Presets</h2>
|
||||
<div class="themes-grid">
|
||||
<div
|
||||
*ngFor="let preset of fontPresets"
|
||||
class="theme-card"
|
||||
[class.active]="preset.key === selectedPreset">
|
||||
|
||||
<h4>{{preset.name}}</h4>
|
||||
<p class="theme-description">{{preset.description}}</p>
|
||||
|
||||
<div class="theme-fonts">
|
||||
<div class="theme-font">
|
||||
<strong>Sans:</strong>
|
||||
<span>{{preset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Serif:</strong>
|
||||
<span>{{preset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Mono:</strong>
|
||||
<span>{{preset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Display:</strong>
|
||||
<span>{{preset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="apply-theme-btn"
|
||||
(click)="applyPreset(preset.key)">
|
||||
Apply {{preset.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Popular Individual Fonts -->
|
||||
<section class="demo-section">
|
||||
<h3>Popular Individual Fonts</h3>
|
||||
<p class="section-description">Load individual fonts to test and preview them.</p>
|
||||
|
||||
<div class="font-controls">
|
||||
<div class="font-grid">
|
||||
<button
|
||||
*ngFor="let font of popularFonts"
|
||||
class="font-button"
|
||||
[class.loaded]="isLoaded(font)"
|
||||
(click)="loadSingleFont(font)">
|
||||
|
||||
{{font}}
|
||||
<span class="load-status" *ngIf="isLoaded(font)">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section" *ngIf="eventLog.length > 0">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div
|
||||
*ngFor="let event of eventLog.slice(-10)"
|
||||
class="event-item"
|
||||
[class]="event.type">
|
||||
<span class="event-time">{{event.time}}</span>
|
||||
<span class="event-message">{{event.message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './font-manager-demo.component.scss'
|
||||
})
|
||||
export class FontManagerDemoComponent implements OnInit, OnDestroy {
|
||||
popularFonts = ['Inter', 'Roboto', 'Playfair Display', 'JetBrains Mono', 'Montserrat', 'Lora'];
|
||||
loadedFonts = new Set<string>();
|
||||
|
||||
// Font preset management
|
||||
fontPresets = [
|
||||
{
|
||||
key: 'modern-clean',
|
||||
name: 'Modern Clean',
|
||||
description: 'Contemporary and minimal design with Inter and Playfair Display',
|
||||
fonts: FONT_COMBINATIONS['Modern Clean']
|
||||
},
|
||||
{
|
||||
key: 'classic-editorial',
|
||||
name: 'Classic Editorial',
|
||||
description: 'Traditional publishing style with Source Sans Pro and Lora',
|
||||
fonts: FONT_COMBINATIONS['Classic Editorial']
|
||||
},
|
||||
{
|
||||
key: 'friendly-modern',
|
||||
name: 'Friendly Modern',
|
||||
description: 'Approachable and warm with Poppins and Merriweather',
|
||||
fonts: FONT_COMBINATIONS['Friendly Modern']
|
||||
},
|
||||
{
|
||||
key: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Corporate and reliable with Roboto and EB Garamond',
|
||||
fonts: FONT_COMBINATIONS['Professional']
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
name: 'System Default',
|
||||
description: 'Use system fonts for optimal performance',
|
||||
fonts: {
|
||||
sans: 'system-ui',
|
||||
serif: 'ui-serif',
|
||||
mono: 'ui-monospace',
|
||||
display: 'system-ui'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
selectedPreset: string = '';
|
||||
selectedTransition: 'fade' | 'scale' | 'typewriter' = 'fade';
|
||||
currentPreset: any = null;
|
||||
|
||||
eventLog: Array<{time: string, message: string, type: 'success' | 'error' | 'info'}> = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.logEvent('Component initialized', 'info');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onThemeChanged(themeName: string): void {
|
||||
this.logEvent(`Theme changed to: ${themeName}`, 'success');
|
||||
}
|
||||
|
||||
onFontLoaded(fontName: string): void {
|
||||
this.logEvent(`Font loaded: ${fontName}`, 'success');
|
||||
this.loadedFonts.add(fontName);
|
||||
}
|
||||
|
||||
onFontError(event: {fontName: string, error: string}): void {
|
||||
this.logEvent(`Font error - ${event.fontName}: ${event.error}`, 'error');
|
||||
}
|
||||
|
||||
loadSingleFont(fontName: string): void {
|
||||
this.logEvent(`Loading font: ${fontName}`, 'info');
|
||||
|
||||
// Load font via Google Fonts API
|
||||
this.loadGoogleFont(fontName);
|
||||
this.loadedFonts.add(fontName);
|
||||
this.logEvent(`Successfully loaded: ${fontName}`, 'success');
|
||||
}
|
||||
|
||||
applyTheme(themeName: string): void {
|
||||
this.logEvent(`Applying theme: ${themeName}`, 'info');
|
||||
this.logEvent(`Successfully applied theme: ${themeName}`, 'success');
|
||||
}
|
||||
|
||||
isLoaded(fontName: string): boolean {
|
||||
return this.loadedFonts.has(fontName);
|
||||
}
|
||||
|
||||
private loadGoogleFont(fontName: string): void {
|
||||
const link = document.createElement('link');
|
||||
link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}&display=swap`;
|
||||
link.rel = 'stylesheet';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
onPresetChange(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
this.selectedPreset = target.value;
|
||||
|
||||
if (this.selectedPreset) {
|
||||
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
|
||||
if (preset) {
|
||||
this.logEvent(`Selected preset: ${preset.name}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyPreset(presetKey: string): void {
|
||||
const preset = this.fontPresets.find(p => p.key === presetKey);
|
||||
if (!preset) {
|
||||
this.logEvent(`Preset not found: ${presetKey}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedPreset = presetKey;
|
||||
this.logEvent(`Applying preset: ${preset.name}`, 'info');
|
||||
|
||||
// Apply fonts to document root for global effect
|
||||
this.applyFontsGlobally(preset.fonts);
|
||||
this.currentPreset = preset;
|
||||
}
|
||||
|
||||
applyPresetWithTransition(): void {
|
||||
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
|
||||
if (!preset) return;
|
||||
|
||||
this.logEvent(`Applying preset "${preset.name}" with ${this.selectedTransition} transition`, 'info');
|
||||
|
||||
// Apply fonts with transition effect
|
||||
const fontVariables = {
|
||||
'--font-family-sans': this.buildGlobalFontStack(preset.fonts.sans),
|
||||
'--font-family-serif': this.buildGlobalFontStack(preset.fonts.serif),
|
||||
'--font-family-mono': this.buildGlobalFontStack(preset.fonts.mono),
|
||||
'--font-family-display': this.buildGlobalFontStack(preset.fonts.display)
|
||||
};
|
||||
|
||||
this.applyFontsWithTransition(fontVariables, this.selectedTransition);
|
||||
this.currentPreset = preset;
|
||||
}
|
||||
|
||||
private applyFontsGlobally(fonts: any): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Set CSS custom properties on the document root
|
||||
root.style.setProperty('--font-family-sans', this.buildGlobalFontStack(fonts.sans));
|
||||
root.style.setProperty('--font-family-serif', this.buildGlobalFontStack(fonts.serif));
|
||||
root.style.setProperty('--font-family-mono', this.buildGlobalFontStack(fonts.mono));
|
||||
root.style.setProperty('--font-family-display', this.buildGlobalFontStack(fonts.display));
|
||||
|
||||
// Also load the fonts if they're Google Fonts
|
||||
if (fonts.sans && fonts.sans !== 'system-ui') {
|
||||
this.loadSingleFont(fonts.sans);
|
||||
}
|
||||
if (fonts.serif && fonts.serif !== 'ui-serif') {
|
||||
this.loadSingleFont(fonts.serif);
|
||||
}
|
||||
if (fonts.mono && fonts.mono !== 'ui-monospace') {
|
||||
this.loadSingleFont(fonts.mono);
|
||||
}
|
||||
if (fonts.display && fonts.display !== 'system-ui') {
|
||||
this.loadSingleFont(fonts.display);
|
||||
}
|
||||
|
||||
this.logEvent(`Applied fonts globally to document root`, 'success');
|
||||
}
|
||||
|
||||
private applyFontsWithTransition(fontVariables: Record<string, string>, transitionType: 'fade' | 'scale' | 'typewriter'): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Add transition class
|
||||
root.classList.add(`font-transition-${transitionType}`);
|
||||
|
||||
// Apply font changes
|
||||
Object.entries(fontVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
|
||||
// Remove transition class after animation
|
||||
setTimeout(() => {
|
||||
root.classList.remove(`font-transition-${transitionType}`);
|
||||
this.logEvent(`${transitionType} transition completed`, 'success');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private buildGlobalFontStack(fontName: string): string {
|
||||
// Handle system fonts
|
||||
if (fontName === 'system-ui') {
|
||||
return 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
}
|
||||
if (fontName === 'ui-serif') {
|
||||
return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
|
||||
}
|
||||
if (fontName === 'ui-monospace') {
|
||||
return 'ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
}
|
||||
|
||||
// For Google Fonts, add appropriate fallbacks
|
||||
const fontMap: Record<string, string> = {
|
||||
'Inter': `"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`,
|
||||
'Roboto': `"Roboto", Arial, Helvetica, sans-serif`,
|
||||
'Poppins': `"Poppins", Arial, Helvetica, sans-serif`,
|
||||
'Source Sans Pro': `"Source Sans Pro", Arial, Helvetica, sans-serif`,
|
||||
'Montserrat': `"Montserrat", Arial, Helvetica, sans-serif`,
|
||||
|
||||
'Playfair Display': `"Playfair Display", Georgia, "Times New Roman", serif`,
|
||||
'Lora': `"Lora", Georgia, "Times New Roman", serif`,
|
||||
'Merriweather': `"Merriweather", Georgia, "Times New Roman", serif`,
|
||||
'EB Garamond': `"EB Garamond", Georgia, "Times New Roman", serif`,
|
||||
|
||||
'JetBrains Mono': `"JetBrains Mono", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Source Code Pro': `"Source Code Pro", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Fira Code': `"Fira Code", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Roboto Mono': `"Roboto Mono", Consolas, Monaco, "Courier New", monospace`,
|
||||
|
||||
'Oswald': `"Oswald", Impact, "Arial Black", sans-serif`,
|
||||
'Raleway': `"Raleway", Arial, Helvetica, sans-serif`
|
||||
};
|
||||
|
||||
return fontMap[fontName] || `"${fontName}", sans-serif`;
|
||||
}
|
||||
|
||||
private logEvent(message: string, type: 'success' | 'error' | 'info'): void {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
this.eventLog.push({ time, message, type });
|
||||
|
||||
// Keep only last 50 events
|
||||
if (this.eventLog.length > 50) {
|
||||
this.eventLog = this.eventLog.slice(-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './font-manager-demo.component';
|
||||
@@ -0,0 +1,274 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
p {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-content-line-tight 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
overflow: hidden;
|
||||
|
||||
.event-controls {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
.no-events {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
text-align: center;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr auto;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-primary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.event-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-content-line-tight;
|
||||
|
||||
.event-time {
|
||||
justify-self: end;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
grid-row: 3;
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { GalleryGridComponent, GalleryGridItem } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-gallery-grid-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, GalleryGridComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Gallery Grid Demo</h2>
|
||||
<p>A responsive media grid with lightbox functionality for organizing and displaying images.</p>
|
||||
|
||||
<!-- Column Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Variants</h3>
|
||||
<div class="demo-grid">
|
||||
@for (columnCount of columnVariants; track columnCount) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ columnCount }} Columns</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems.slice(0, 6)"
|
||||
[columns]="columnCount">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (gap of gapSizes; track gap) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ gap | titlecase }} Gap</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems.slice(0, 6)"
|
||||
[columns]="3"
|
||||
[gap]="gap">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Object Fit Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Object Fit Options</h3>
|
||||
<div class="demo-grid">
|
||||
@for (fit of objectFitOptions; track fit) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ fit | titlecase }} Fit</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="landscapeItems"
|
||||
[columns]="3"
|
||||
[objectFit]="fit">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto-fit Responsive -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto-fit Responsive</h3>
|
||||
<p>Automatically adjusts columns based on available space (resize window to see effect):</p>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems"
|
||||
columns="auto-fit"
|
||||
[gap]="'md'">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Masonry Layout -->
|
||||
<section class="demo-section">
|
||||
<h3>Masonry Layout</h3>
|
||||
<p>Items align to top with varying heights:</p>
|
||||
<ui-gallery-grid
|
||||
[items]="masonryItems"
|
||||
[columns]="4"
|
||||
[masonry]="true"
|
||||
[gap]="'sm'">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button (click)="toggleOverlay()" [class.active]="showOverlay">
|
||||
{{ showOverlay ? 'Hide' : 'Show' }} Overlay
|
||||
</button>
|
||||
<button (click)="toggleZoomIndicator()" [class.active]="showZoomIndicator">
|
||||
{{ showZoomIndicator ? 'Hide' : 'Show' }} Zoom Indicator
|
||||
</button>
|
||||
<button (click)="toggleSelection()" [class.active]="selectionEnabled">
|
||||
{{ selectionEnabled ? 'Disable' : 'Enable' }} Selection
|
||||
</button>
|
||||
<button (click)="selectAll()" [disabled]="!selectionEnabled">
|
||||
Select All
|
||||
</button>
|
||||
<button (click)="deselectAll()" [disabled]="!selectionEnabled">
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-gallery-grid
|
||||
[items]="interactiveItems"
|
||||
[columns]="4"
|
||||
[showOverlay]="showOverlay"
|
||||
[showZoomIndicator]="showZoomIndicator"
|
||||
(itemClick)="handleItemClick($event)"
|
||||
(itemSelect)="handleItemSelect($event)"
|
||||
(imageLoad)="handleImageLoad($event)"
|
||||
(imageError)="handleImageError($event)">
|
||||
</ui-gallery-grid>
|
||||
|
||||
@if (selectedItems.length > 0) {
|
||||
<div class="selection-info">
|
||||
<p>Selected Items: {{ selectedItems.length }}</p>
|
||||
<ul>
|
||||
@for (item of selectedItems; track item.id) {
|
||||
<li>{{ item.title || item.alt || 'Item ' + item.id }}</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Empty State -->
|
||||
<section class="demo-section">
|
||||
<h3>Empty State</h3>
|
||||
<ui-gallery-grid
|
||||
[items]="[]"
|
||||
[columns]="3"
|
||||
emptyText="No photos in this album yet">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="demo-section">
|
||||
<h3>Loading State</h3>
|
||||
<ui-gallery-grid
|
||||
[items]="loadingItems"
|
||||
[columns]="3">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div class="event-controls">
|
||||
<button (click)="clearEventLog()">Clear Log</button>
|
||||
</div>
|
||||
<div class="event-list">
|
||||
@if (eventLog.length === 0) {
|
||||
<p class="no-events">No events yet. Interact with the gallery above to see events.</p>
|
||||
} @else {
|
||||
@for (event of eventLog; track event.id) {
|
||||
<div class="event-item">
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span class="event-details">{{ event.details }}</span>
|
||||
<span class="event-time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './gallery-grid-demo.component.scss'
|
||||
})
|
||||
export class GalleryGridDemoComponent {
|
||||
columnVariants = [2, 3, 4] as const;
|
||||
gapSizes = ['sm', 'md', 'lg'] as const;
|
||||
objectFitOptions = ['cover', 'contain', 'fill'] as const;
|
||||
|
||||
showOverlay = true;
|
||||
showZoomIndicator = true;
|
||||
selectionEnabled = false;
|
||||
selectedItems: GalleryGridItem[] = [];
|
||||
eventLog: Array<{id: number; type: string; details: string; timestamp: Date}> = [];
|
||||
|
||||
sampleItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
src: 'https://picsum.photos/400/300?random=1',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=1',
|
||||
alt: 'Sample image 1',
|
||||
title: 'Mountain Landscape',
|
||||
caption: 'Beautiful mountain vista at sunset'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
src: 'https://picsum.photos/400/300?random=2',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=2',
|
||||
alt: 'Sample image 2',
|
||||
title: 'Ocean Waves',
|
||||
caption: 'Crashing waves on a sandy beach'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
src: 'https://picsum.photos/400/300?random=3',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=3',
|
||||
alt: 'Sample image 3',
|
||||
title: 'Forest Path',
|
||||
caption: 'Winding trail through dense woods'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
src: 'https://picsum.photos/400/300?random=4',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=4',
|
||||
alt: 'Sample image 4',
|
||||
title: 'City Skyline',
|
||||
caption: 'Urban architecture at night'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
src: 'https://picsum.photos/400/300?random=5',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=5',
|
||||
alt: 'Sample image 5',
|
||||
title: 'Desert Dunes',
|
||||
caption: 'Rolling sand dunes in morning light'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
src: 'https://picsum.photos/400/300?random=6',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=6',
|
||||
alt: 'Sample image 6',
|
||||
title: 'Lake Reflection',
|
||||
caption: 'Mirror-like water surface'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
src: 'https://picsum.photos/400/300?random=7',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=7',
|
||||
alt: 'Sample image 7',
|
||||
title: 'Flower Field',
|
||||
caption: 'Colorful wildflowers in bloom'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
src: 'https://picsum.photos/400/300?random=8',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=8',
|
||||
alt: 'Sample image 8',
|
||||
title: 'Snow Peaks',
|
||||
caption: 'Snow-capped mountain range'
|
||||
}
|
||||
];
|
||||
|
||||
landscapeItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'l1',
|
||||
src: 'https://picsum.photos/800/400?random=11',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=11',
|
||||
alt: 'Landscape image 1',
|
||||
title: 'Wide Landscape',
|
||||
caption: 'Panoramic view'
|
||||
},
|
||||
{
|
||||
id: 'l2',
|
||||
src: 'https://picsum.photos/800/400?random=12',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=12',
|
||||
alt: 'Landscape image 2',
|
||||
title: 'River Valley',
|
||||
caption: 'Flowing water through hills'
|
||||
},
|
||||
{
|
||||
id: 'l3',
|
||||
src: 'https://picsum.photos/800/400?random=13',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=13',
|
||||
alt: 'Landscape image 3',
|
||||
title: 'Coastal View',
|
||||
caption: 'Rocky coastline'
|
||||
}
|
||||
];
|
||||
|
||||
masonryItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'm1',
|
||||
src: 'https://picsum.photos/300/400?random=21',
|
||||
thumbnail: 'https://picsum.photos/150/200?random=21',
|
||||
alt: 'Tall image 1',
|
||||
title: 'Portrait 1'
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
src: 'https://picsum.photos/300/200?random=22',
|
||||
thumbnail: 'https://picsum.photos/150/100?random=22',
|
||||
alt: 'Wide image 1',
|
||||
title: 'Landscape 1'
|
||||
},
|
||||
{
|
||||
id: 'm3',
|
||||
src: 'https://picsum.photos/300/500?random=23',
|
||||
thumbnail: 'https://picsum.photos/150/250?random=23',
|
||||
alt: 'Very tall image',
|
||||
title: 'Portrait 2'
|
||||
},
|
||||
{
|
||||
id: 'm4',
|
||||
src: 'https://picsum.photos/300/300?random=24',
|
||||
thumbnail: 'https://picsum.photos/150/150?random=24',
|
||||
alt: 'Square image',
|
||||
title: 'Square 1'
|
||||
},
|
||||
{
|
||||
id: 'm5',
|
||||
src: 'https://picsum.photos/300/350?random=25',
|
||||
thumbnail: 'https://picsum.photos/150/175?random=25',
|
||||
alt: 'Tall image 2',
|
||||
title: 'Portrait 3'
|
||||
},
|
||||
{
|
||||
id: 'm6',
|
||||
src: 'https://picsum.photos/300/250?random=26',
|
||||
thumbnail: 'https://picsum.photos/150/125?random=26',
|
||||
alt: 'Medium image',
|
||||
title: 'Landscape 2'
|
||||
}
|
||||
];
|
||||
|
||||
interactiveItems: GalleryGridItem[] = this.sampleItems.slice(0, 8).map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
|
||||
loadingItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'loading1',
|
||||
src: 'https://picsum.photos/400/300?random=31',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=31',
|
||||
alt: 'Loading image 1',
|
||||
title: 'Loading Item 1',
|
||||
loading: true
|
||||
},
|
||||
{
|
||||
id: 'loading2',
|
||||
src: 'https://picsum.photos/400/300?random=32',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=32',
|
||||
alt: 'Loading image 2',
|
||||
title: 'Loading Item 2',
|
||||
loading: true
|
||||
},
|
||||
{
|
||||
id: 'loading3',
|
||||
src: 'https://picsum.photos/400/300?random=33',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=33',
|
||||
alt: 'Loading image 3',
|
||||
title: 'Loaded Item',
|
||||
loading: false
|
||||
}
|
||||
];
|
||||
|
||||
private eventCounter = 0;
|
||||
|
||||
toggleOverlay(): void {
|
||||
this.showOverlay = !this.showOverlay;
|
||||
}
|
||||
|
||||
toggleZoomIndicator(): void {
|
||||
this.showZoomIndicator = !this.showZoomIndicator;
|
||||
}
|
||||
|
||||
toggleSelection(): void {
|
||||
this.selectionEnabled = !this.selectionEnabled;
|
||||
if (!this.selectionEnabled) {
|
||||
this.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(): void {
|
||||
this.interactiveItems.forEach(item => item.selected = true);
|
||||
this.updateSelectedItems();
|
||||
}
|
||||
|
||||
deselectAll(): void {
|
||||
this.interactiveItems.forEach(item => item.selected = false);
|
||||
this.updateSelectedItems();
|
||||
}
|
||||
|
||||
handleItemClick(event: { item: GalleryGridItem; event: MouseEvent }): void {
|
||||
this.addEvent('Item Click', `Clicked item: ${event.item.title || event.item.id}`);
|
||||
|
||||
if (this.selectionEnabled) {
|
||||
event.item.selected = !event.item.selected;
|
||||
this.updateSelectedItems();
|
||||
this.addEvent('Item Selection', `${event.item.selected ? 'Selected' : 'Deselected'} item: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleItemSelect(item: GalleryGridItem): void {
|
||||
this.updateSelectedItems();
|
||||
this.addEvent('Item Select', `Selected item: ${item.title || item.id}`);
|
||||
}
|
||||
|
||||
handleImageLoad(event: { item: GalleryGridItem; event: Event }): void {
|
||||
this.addEvent('Image Load', `Loaded image: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
|
||||
handleImageError(event: { item: GalleryGridItem; event: Event }): void {
|
||||
this.addEvent('Image Error', `Failed to load image: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog = [];
|
||||
}
|
||||
|
||||
private updateSelectedItems(): void {
|
||||
this.selectedItems = this.interactiveItems.filter(item => item.selected);
|
||||
}
|
||||
|
||||
private addEvent(type: string, details: string): void {
|
||||
this.eventLog.unshift({
|
||||
id: ++this.eventCounter,
|
||||
type,
|
||||
details,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Keep only the last 20 events
|
||||
if (this.eventLog.length > 20) {
|
||||
this.eventLog = this.eventLog.slice(0, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>hcl-studio-demo works!</p>
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* HCL Studio Demo Styles
|
||||
* Showcases dynamic theming using CSS custom properties
|
||||
*/
|
||||
|
||||
.hcl-studio-demo {
|
||||
padding: 2rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// HEADER STYLES
|
||||
// ==========================================================================
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface-container);
|
||||
border-radius: 1rem;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-on-surface-variant);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CURRENT THEME INFO
|
||||
// ==========================================================================
|
||||
|
||||
.current-theme-info {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-surface-high);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.color-swatches {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.swatch {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// MODE CONTROLS
|
||||
// ==========================================================================
|
||||
|
||||
.mode-controls {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
|
||||
.mode-toggle-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 2rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-high);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// THEMES SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.themes-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.theme-card {
|
||||
padding: 1rem;
|
||||
background: var(--color-surface-container);
|
||||
border: 2px solid var(--color-outline-variant);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-container);
|
||||
|
||||
.theme-name {
|
||||
color: var(--color-on-primary-container);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.color-strip {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CUSTOM THEME SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.custom-theme-section {
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface-high);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-inputs {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.color-input-group {
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-on-surface);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.preview-btn {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-on-secondary);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.apply-btn {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.preview-swatches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 0.5rem;
|
||||
|
||||
.preview-swatch {
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DEMO COMPONENTS
|
||||
// ==========================================================================
|
||||
|
||||
.demo-components {
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.component-showcase {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.showcase-section {
|
||||
padding: 1.5rem;
|
||||
background: var(--color-surface-container);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
// Button Styles
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-on-secondary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-tertiary {
|
||||
background: var(--color-tertiary);
|
||||
color: var(--color-on-tertiary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card Styles
|
||||
.card-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.demo-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&.primary {
|
||||
background: var(--color-primary-container);
|
||||
color: var(--color-on-primary-container);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--color-secondary-container);
|
||||
color: var(--color-on-secondary-container);
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Surface Styles
|
||||
.surface-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.demo-surface {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
|
||||
&.surface-low {
|
||||
background: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
&.surface-container {
|
||||
background: var(--color-surface-container);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
&.surface-high {
|
||||
background: var(--color-surface-high);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// RESPONSIVE ADJUSTMENTS
|
||||
// ==========================================================================
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hcl-studio-demo {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.custom-inputs {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// TRANSITIONS FOR SMOOTH THEME CHANGES
|
||||
// ==========================================================================
|
||||
|
||||
* {
|
||||
transition: background-color 0.3s ease,
|
||||
color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HclStudioDemoComponent } from './hcl-studio-demo.component';
|
||||
|
||||
describe('HclStudioDemoComponent', () => {
|
||||
let component: HclStudioDemoComponent;
|
||||
let fixture: ComponentFixture<HclStudioDemoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HclStudioDemoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HclStudioDemoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
HCLStudioService,
|
||||
DEFAULT_THEMES,
|
||||
ThemePreview,
|
||||
BrandColors,
|
||||
HCLConverter,
|
||||
PaletteGenerator
|
||||
} from 'hcl-studio';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hcl-studio-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<div class="hcl-studio-demo">
|
||||
<div class="demo-header">
|
||||
<h1>HCL Studio Demo</h1>
|
||||
<p>Dynamic theme switching using HCL color space</p>
|
||||
</div>
|
||||
|
||||
<!-- Current Theme Info -->
|
||||
<div class="current-theme-info" *ngIf="currentTheme">
|
||||
<h3>Current Theme: {{ currentTheme.name }}</h3>
|
||||
<div class="color-swatches">
|
||||
<div class="swatch" [style.background]="currentTheme.primary">
|
||||
<span>Primary</span>
|
||||
</div>
|
||||
<div class="swatch" [style.background]="currentTheme.secondary">
|
||||
<span>Secondary</span>
|
||||
</div>
|
||||
<div class="swatch" [style.background]="currentTheme.tertiary">
|
||||
<span>Tertiary</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Toggle -->
|
||||
<div class="mode-controls">
|
||||
<button
|
||||
class="mode-toggle-btn"
|
||||
(click)="toggleMode()"
|
||||
[class.dark]="isDarkMode"
|
||||
>
|
||||
{{ isDarkMode ? 'Switch to Light' : 'Switch to Dark' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Built-in Themes -->
|
||||
<div class="themes-section">
|
||||
<h3>Built-in Themes</h3>
|
||||
<div class="theme-grid">
|
||||
<div
|
||||
*ngFor="let theme of availableThemes"
|
||||
class="theme-card"
|
||||
[class.active]="theme.id === currentTheme?.id"
|
||||
(click)="switchTheme(theme.id)"
|
||||
>
|
||||
<div class="theme-preview">
|
||||
<div class="color-strip" [style.background]="theme.primary"></div>
|
||||
<div class="color-strip" [style.background]="theme.secondary"></div>
|
||||
<div class="color-strip" [style.background]="theme.tertiary"></div>
|
||||
</div>
|
||||
<span class="theme-name">{{ theme.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Color Input -->
|
||||
<div class="custom-theme-section">
|
||||
<h3>Create Custom Theme</h3>
|
||||
<div class="custom-inputs">
|
||||
<div class="color-input-group">
|
||||
<label>Primary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.primary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.primary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#6750A4"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="color-input-group">
|
||||
<label>Secondary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.secondary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.secondary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#625B71"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="color-input-group">
|
||||
<label>Tertiary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.tertiary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.tertiary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#7D5260"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-actions">
|
||||
<button class="preview-btn" (click)="previewCustomTheme()">
|
||||
Preview Theme
|
||||
</button>
|
||||
<button class="apply-btn" (click)="applyCustomTheme()">
|
||||
Apply Theme
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Color Preview -->
|
||||
<div *ngIf="colorPreview" class="color-preview">
|
||||
<h4>Generated Palette Preview</h4>
|
||||
<div class="preview-swatches">
|
||||
<div
|
||||
*ngFor="let color of colorPreview | keyvalue"
|
||||
class="preview-swatch"
|
||||
[style.background]="color.value"
|
||||
[style.color]="getContrastColor(color.value)"
|
||||
>
|
||||
{{ color.key }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo Components -->
|
||||
<div class="demo-components">
|
||||
<h3>Theme Demo Components</h3>
|
||||
|
||||
<div class="component-showcase">
|
||||
<!-- Buttons -->
|
||||
<div class="showcase-section">
|
||||
<h4>Buttons</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn-primary">Primary Button</button>
|
||||
<button class="btn-secondary">Secondary Button</button>
|
||||
<button class="btn-tertiary">Tertiary Button</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="showcase-section">
|
||||
<h4>Cards</h4>
|
||||
<div class="card-group">
|
||||
<div class="demo-card primary">
|
||||
<h5>Primary Card</h5>
|
||||
<p>This card uses primary colors</p>
|
||||
</div>
|
||||
<div class="demo-card secondary">
|
||||
<h5>Secondary Card</h5>
|
||||
<p>This card uses secondary colors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<div class="showcase-section">
|
||||
<h4>Surface Variations</h4>
|
||||
<div class="surface-group">
|
||||
<div class="demo-surface surface-low">Surface Low</div>
|
||||
<div class="demo-surface surface-container">Surface Container</div>
|
||||
<div class="demo-surface surface-high">Surface High</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./hcl-studio-demo.component.scss']
|
||||
})
|
||||
export class HclStudioDemoComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// State
|
||||
availableThemes: ThemePreview[] = [];
|
||||
currentTheme: ThemePreview | null = null;
|
||||
isDarkMode = false;
|
||||
|
||||
// Custom theme inputs
|
||||
customColors: BrandColors = {
|
||||
primary: '#6750A4',
|
||||
secondary: '#625B71',
|
||||
tertiary: '#7D5260'
|
||||
};
|
||||
|
||||
colorPreview: { [key: string]: string } | null = null;
|
||||
|
||||
constructor(private hclStudio: HCLStudioService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize HCL Studio with default themes
|
||||
this.hclStudio.initialize({
|
||||
themes: DEFAULT_THEMES,
|
||||
defaultTheme: 'material-purple',
|
||||
autoMode: false
|
||||
});
|
||||
|
||||
// Subscribe to theme changes
|
||||
this.hclStudio.themeState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(state => {
|
||||
this.availableThemes = state.availableThemes;
|
||||
if (state.currentTheme) {
|
||||
this.currentTheme = {
|
||||
id: state.currentTheme.config.id,
|
||||
name: state.currentTheme.config.name,
|
||||
description: state.currentTheme.config.description,
|
||||
primary: state.currentTheme.config.colors.primary,
|
||||
secondary: state.currentTheme.config.colors.secondary,
|
||||
tertiary: state.currentTheme.config.colors.tertiary
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to mode changes
|
||||
this.hclStudio.currentMode$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(mode => {
|
||||
this.isDarkMode = mode === 'dark';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
switchTheme(themeId: string): void {
|
||||
this.hclStudio.switchTheme(themeId);
|
||||
}
|
||||
|
||||
toggleMode(): void {
|
||||
this.hclStudio.toggleMode();
|
||||
}
|
||||
|
||||
previewCustomTheme(): void {
|
||||
try {
|
||||
this.colorPreview = this.hclStudio.generateColorPreview(this.customColors);
|
||||
} catch (error) {
|
||||
console.warn('Failed to generate color preview:', error);
|
||||
this.colorPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
applyCustomTheme(): void {
|
||||
const customTheme = this.hclStudio.createCustomTheme(
|
||||
'custom-theme',
|
||||
'My Custom Theme',
|
||||
this.customColors,
|
||||
'User-created custom theme'
|
||||
);
|
||||
|
||||
this.hclStudio.switchTheme('custom-theme');
|
||||
}
|
||||
|
||||
getContrastColor(backgroundColor: string): string {
|
||||
try {
|
||||
const ratio1 = HCLConverter.getContrastRatio(backgroundColor, '#000000');
|
||||
const ratio2 = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
|
||||
return ratio1 > ratio2 ? '#000000' : '#FFFFFF';
|
||||
} catch {
|
||||
return '#000000';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
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-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-showcase {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
height: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.demo-scroll-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Basic demo items
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-line-normal 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
}
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// Template demo items
|
||||
.demo-template-item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&--last {
|
||||
border-color: $semantic-color-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__id {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&__category {
|
||||
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);
|
||||
background: $semantic-color-surface-container;
|
||||
color: $semantic-color-text-secondary;
|
||||
padding: $semantic-spacing-1 $semantic-spacing-2;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&__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: 0 0 $semantic-spacing-component-xs 0;
|
||||
}
|
||||
|
||||
&__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;
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-top: $semantic-spacing-component-xs;
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bidirectional demo items
|
||||
.demo-bidirectional-item {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
&__content {
|
||||
background: $semantic-color-surface-elevated;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border-left: 4px solid $semantic-color-primary;
|
||||
|
||||
strong {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
display: block;
|
||||
margin-bottom: $semantic-spacing-content-line-tight;
|
||||
}
|
||||
|
||||
p {
|
||||
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;
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
}
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom templates
|
||||
.demo-custom-item {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&__avatar {
|
||||
flex-shrink: 0;
|
||||
width: $semantic-sizing-touch-target;
|
||||
height: $semantic-sizing-touch-target;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
}
|
||||
|
||||
p {
|
||||
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;
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-custom-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: linear-gradient(45deg, $semantic-color-primary, $semantic-color-secondary);
|
||||
color: $semantic-color-on-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
&__spinner {
|
||||
width: $semantic-sizing-icon-button;
|
||||
height: $semantic-sizing-icon-button;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin $semantic-motion-duration-slow linear infinite;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-custom-error {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: rgba($semantic-color-danger, 0.1);
|
||||
border: $semantic-border-width-1 solid $semantic-color-danger;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
&__icon {
|
||||
font-size: $semantic-typography-font-size-2xl;
|
||||
margin-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-danger;
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-custom-end {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: linear-gradient(135deg, $semantic-color-success, $semantic-color-primary);
|
||||
color: $semantic-color-on-success;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
&__icon {
|
||||
font-size: $semantic-typography-font-size-3xl;
|
||||
margin-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);
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration demo
|
||||
.demo-config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&__index {
|
||||
flex-shrink: 0;
|
||||
width: $semantic-sizing-touch-minimum;
|
||||
height: $semantic-sizing-touch-minimum;
|
||||
background: $semantic-color-surface-container;
|
||||
color: $semantic-color-text-secondary;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
}
|
||||
|
||||
p {
|
||||
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;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
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-radius-sm;
|
||||
cursor: pointer;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
label {
|
||||
font-family: map-get($semantic-typography-label, font-family);
|
||||
font-size: map-get($semantic-typography-label, font-size);
|
||||
font-weight: map-get($semantic-typography-label, font-weight);
|
||||
line-height: map-get($semantic-typography-label, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.demo-template-item {
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-custom-loading__spinner {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { InfiniteScrollContainerComponent, InfiniteScrollEvent, InfiniteScrollConfig } from 'ui-essentials';
|
||||
|
||||
interface DemoItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
category: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-infinite-scroll-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, InfiniteScrollContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Infinite Scroll Container Demo</h2>
|
||||
|
||||
<!-- Basic Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Infinite Scroll (Content Projection)</h3>
|
||||
<div class="demo-showcase">
|
||||
<ui-infinite-scroll-container
|
||||
class="demo-scroll-container"
|
||||
[hasMore]="basicHasMore()"
|
||||
[loading]="basicLoading()"
|
||||
(loadMore)="loadMoreBasic($event)">
|
||||
|
||||
@for (item of basicItems(); track item.id) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.description }}</p>
|
||||
<small>{{ item.timestamp | date:'short' }} - {{ item.category }}</small>
|
||||
</div>
|
||||
}
|
||||
</ui-infinite-scroll-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Template-Based Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Template-Based Rendering</h3>
|
||||
<div class="demo-showcase">
|
||||
<ui-infinite-scroll-container
|
||||
class="demo-scroll-container"
|
||||
[items]="templateItems()"
|
||||
[itemTemplate]="itemTemplate"
|
||||
[trackByFn]="trackByFn"
|
||||
[hasMore]="templateHasMore()"
|
||||
[loading]="templateLoading()"
|
||||
[config]="templateConfig"
|
||||
(loadMore)="loadMoreTemplate($event)">
|
||||
</ui-infinite-scroll-container>
|
||||
|
||||
<ng-template #itemTemplate let-item="item" let-index="index" let-isLast="isLast">
|
||||
<div class="demo-template-item" [class.demo-template-item--last]="isLast">
|
||||
<div class="demo-template-item__header">
|
||||
<span class="demo-template-item__id">#{{ item.id }}</span>
|
||||
<span class="demo-template-item__category">{{ item.category }}</span>
|
||||
</div>
|
||||
<h4 class="demo-template-item__title">{{ item.title }}</h4>
|
||||
<p class="demo-template-item__description">{{ item.description }}</p>
|
||||
<div class="demo-template-item__footer">
|
||||
<small>Index: {{ index }} | {{ item.timestamp | date:'short' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bidirectional Scrolling -->
|
||||
<section class="demo-section">
|
||||
<h3>Bidirectional Infinite Scroll</h3>
|
||||
<div class="demo-showcase">
|
||||
<ui-infinite-scroll-container
|
||||
class="demo-scroll-container"
|
||||
direction="both"
|
||||
[items]="bidirectionalItems()"
|
||||
[itemTemplate]="bidirectionalTemplate"
|
||||
[hasMore]="bidirectionalHasMore()"
|
||||
[loadingUp]="bidirectionalLoadingUp()"
|
||||
[loadingDown]="bidirectionalLoadingDown()"
|
||||
[config]="bidirectionalConfig"
|
||||
(loadMore)="loadMoreBidirectional($event)">
|
||||
</ui-infinite-scroll-container>
|
||||
|
||||
<ng-template #bidirectionalTemplate let-item="item" let-index="index">
|
||||
<div class="demo-bidirectional-item">
|
||||
<div class="demo-bidirectional-item__content">
|
||||
<strong>Message {{ item.id }}</strong>
|
||||
<p>{{ item.description }}</p>
|
||||
<small>{{ item.timestamp | date:'short' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Templates -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Loading & Error Templates</h3>
|
||||
<div class="demo-showcase">
|
||||
<ui-infinite-scroll-container
|
||||
class="demo-scroll-container"
|
||||
[items]="customItems()"
|
||||
[itemTemplate]="customItemTemplate"
|
||||
[loadingTemplate]="customLoadingTemplate"
|
||||
[errorTemplate]="customErrorTemplate"
|
||||
[endTemplate]="customEndTemplate"
|
||||
[hasMore]="customHasMore()"
|
||||
[loading]="customLoading()"
|
||||
[error]="customError()"
|
||||
[config]="customConfig"
|
||||
(loadMore)="loadMoreCustom($event)"
|
||||
(retryRequested)="retryCustom()">
|
||||
</ui-infinite-scroll-container>
|
||||
|
||||
<ng-template #customItemTemplate let-item="item">
|
||||
<div class="demo-custom-item">
|
||||
<div class="demo-custom-item__avatar">
|
||||
{{ item.title.charAt(0) }}
|
||||
</div>
|
||||
<div class="demo-custom-item__content">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.description }}</p>
|
||||
<span class="demo-custom-item__time">{{ getTimeAgo(item.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #customLoadingTemplate>
|
||||
<div class="demo-custom-loading">
|
||||
<div class="demo-custom-loading__spinner"></div>
|
||||
<span>Loading amazing content...</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #customErrorTemplate let-error="error">
|
||||
<div class="demo-custom-error">
|
||||
<div class="demo-custom-error__icon">⚠️</div>
|
||||
<h4>Oops! Something went wrong</h4>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #customEndTemplate>
|
||||
<div class="demo-custom-end">
|
||||
<div class="demo-custom-end__icon">🎉</div>
|
||||
<h4>That's all folks!</h4>
|
||||
<p>You've reached the end of our amazing content.</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
<div class="demo-controls">
|
||||
<div class="demo-control">
|
||||
<label>
|
||||
<input type="checkbox" [checked]="disableScroll()" (change)="toggleDisabled($event)">
|
||||
Disable Infinite Scroll
|
||||
</label>
|
||||
</div>
|
||||
<div class="demo-control">
|
||||
<label>
|
||||
Threshold: {{ threshold() }}px
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="500"
|
||||
[value]="threshold()"
|
||||
(input)="updateThreshold($event)">
|
||||
</label>
|
||||
</div>
|
||||
<div class="demo-control">
|
||||
<label>
|
||||
Debounce Time: {{ debounceTime() }}ms
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="1000"
|
||||
[value]="debounceTime()"
|
||||
(input)="updateDebounceTime($event)">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-showcase">
|
||||
<ui-infinite-scroll-container
|
||||
class="demo-scroll-container"
|
||||
[items]="configItems()"
|
||||
[itemTemplate]="configItemTemplate"
|
||||
[hasMore]="configHasMore()"
|
||||
[loading]="configLoading()"
|
||||
[config]="currentConfig()"
|
||||
(loadMore)="loadMoreConfig($event)">
|
||||
</ui-infinite-scroll-container>
|
||||
|
||||
<ng-template #configItemTemplate let-item="item" let-index="index">
|
||||
<div class="demo-config-item">
|
||||
<span class="demo-config-item__index">{{ index + 1 }}</span>
|
||||
<div class="demo-config-item__content">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Methods Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>API Methods</h3>
|
||||
<div class="demo-controls">
|
||||
<button type="button" (click)="scrollToTop()">Scroll to Top</button>
|
||||
<button type="button" (click)="scrollToBottom()">Scroll to Bottom</button>
|
||||
<button type="button" (click)="scrollToIndex(50)">Scroll to Item 50</button>
|
||||
<button type="button" (click)="resetApiScroll()">Reset Scroll</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './infinite-scroll-container-demo.component.scss'
|
||||
})
|
||||
export class InfiniteScrollContainerDemoComponent {
|
||||
@ViewChild('itemTemplate') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
// Basic example
|
||||
basicItems = signal<DemoItem[]>([]);
|
||||
basicLoading = signal(false);
|
||||
basicHasMore = signal(true);
|
||||
private basicPage = 1;
|
||||
|
||||
// Template example
|
||||
templateItems = signal<DemoItem[]>([]);
|
||||
templateLoading = signal(false);
|
||||
templateHasMore = signal(true);
|
||||
private templatePage = 1;
|
||||
|
||||
templateConfig: InfiniteScrollConfig = {
|
||||
threshold: 100,
|
||||
debounceTime: 150,
|
||||
itemHeight: 120
|
||||
};
|
||||
|
||||
// Bidirectional example
|
||||
bidirectionalItems = signal<DemoItem[]>([]);
|
||||
bidirectionalLoadingUp = signal(false);
|
||||
bidirectionalLoadingDown = signal(false);
|
||||
bidirectionalHasMore = signal(true);
|
||||
private bidirectionalTopPage = 0;
|
||||
private bidirectionalBottomPage = 1;
|
||||
|
||||
bidirectionalConfig: InfiniteScrollConfig = {
|
||||
threshold: 50,
|
||||
debounceTime: 200
|
||||
};
|
||||
|
||||
// Custom templates example
|
||||
customItems = signal<DemoItem[]>([]);
|
||||
customLoading = signal(false);
|
||||
customHasMore = signal(true);
|
||||
customError = signal<string | undefined>(undefined);
|
||||
private customPage = 1;
|
||||
private customFailCount = 0;
|
||||
|
||||
customConfig: InfiniteScrollConfig = {
|
||||
threshold: 150,
|
||||
debounceTime: 100
|
||||
};
|
||||
|
||||
// Configuration example
|
||||
configItems = signal<DemoItem[]>([]);
|
||||
configLoading = signal(false);
|
||||
configHasMore = signal(true);
|
||||
private configPage = 1;
|
||||
|
||||
// Configuration controls
|
||||
threshold = signal(200);
|
||||
debounceTime = signal(300);
|
||||
disableScroll = signal(false);
|
||||
|
||||
currentConfig = signal<InfiniteScrollConfig>({
|
||||
threshold: 200,
|
||||
debounceTime: 300,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Initialize with some data
|
||||
this.loadInitialData();
|
||||
}
|
||||
|
||||
// Track by function for template rendering
|
||||
trackByFn = (index: number, item: DemoItem) => item.id;
|
||||
|
||||
// Basic example methods
|
||||
loadMoreBasic(event: InfiniteScrollEvent): void {
|
||||
if (this.basicLoading()) return;
|
||||
|
||||
this.basicLoading.set(true);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(this.basicPage * 20, 20, 'basic');
|
||||
this.basicItems.update(items => [...items, ...newItems]);
|
||||
this.basicPage++;
|
||||
this.basicLoading.set(false);
|
||||
|
||||
// Stop loading after 5 pages
|
||||
if (this.basicPage > 5) {
|
||||
this.basicHasMore.set(false);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Template example methods
|
||||
loadMoreTemplate(event: InfiniteScrollEvent): void {
|
||||
if (this.templateLoading()) return;
|
||||
|
||||
this.templateLoading.set(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(this.templatePage * 15, 15, 'template');
|
||||
this.templateItems.update(items => [...items, ...newItems]);
|
||||
this.templatePage++;
|
||||
this.templateLoading.set(false);
|
||||
|
||||
if (this.templatePage > 6) {
|
||||
this.templateHasMore.set(false);
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// Bidirectional example methods
|
||||
loadMoreBidirectional(event: InfiniteScrollEvent): void {
|
||||
if (event.direction === 'down') {
|
||||
this.loadMoreBidirectionalDown();
|
||||
} else {
|
||||
this.loadMoreBidirectionalUp();
|
||||
}
|
||||
}
|
||||
|
||||
private loadMoreBidirectionalDown(): void {
|
||||
if (this.bidirectionalLoadingDown()) return;
|
||||
|
||||
this.bidirectionalLoadingDown.set(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(this.bidirectionalBottomPage * 10, 10, 'chat');
|
||||
this.bidirectionalItems.update(items => [...items, ...newItems]);
|
||||
this.bidirectionalBottomPage++;
|
||||
this.bidirectionalLoadingDown.set(false);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
private loadMoreBidirectionalUp(): void {
|
||||
if (this.bidirectionalLoadingUp()) return;
|
||||
|
||||
this.bidirectionalLoadingUp.set(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const startId = this.bidirectionalTopPage * 10;
|
||||
const newItems = this.generateItems(startId, 10, 'history');
|
||||
this.bidirectionalItems.update(items => [...newItems, ...items]);
|
||||
this.bidirectionalTopPage--;
|
||||
this.bidirectionalLoadingUp.set(false);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// Custom templates example methods
|
||||
loadMoreCustom(event: InfiniteScrollEvent): void {
|
||||
if (this.customLoading()) return;
|
||||
|
||||
this.customLoading.set(true);
|
||||
this.customError.set(undefined);
|
||||
|
||||
// Simulate occasional failures
|
||||
const shouldFail = Math.random() < 0.3 && this.customFailCount < 2;
|
||||
|
||||
setTimeout(() => {
|
||||
if (shouldFail) {
|
||||
this.customError.set('Network error: Failed to load more items');
|
||||
this.customLoading.set(false);
|
||||
this.customFailCount++;
|
||||
} else {
|
||||
const newItems = this.generateItems(this.customPage * 12, 12, 'custom');
|
||||
this.customItems.update(items => [...items, ...newItems]);
|
||||
this.customPage++;
|
||||
this.customLoading.set(false);
|
||||
|
||||
if (this.customPage > 4) {
|
||||
this.customHasMore.set(false);
|
||||
}
|
||||
}
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
retryCustom(): void {
|
||||
this.loadMoreCustom({ direction: 'down', currentIndex: 0, scrollPosition: 0 });
|
||||
}
|
||||
|
||||
// Configuration example methods
|
||||
loadMoreConfig(event: InfiniteScrollEvent): void {
|
||||
if (this.configLoading()) return;
|
||||
|
||||
this.configLoading.set(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(this.configPage * 10, 10, 'config');
|
||||
this.configItems.update(items => [...items, ...newItems]);
|
||||
this.configPage++;
|
||||
this.configLoading.set(false);
|
||||
|
||||
if (this.configPage > 10) {
|
||||
this.configHasMore.set(false);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Configuration controls
|
||||
toggleDisabled(event: Event): void {
|
||||
const disabled = (event.target as HTMLInputElement).checked;
|
||||
this.disableScroll.set(disabled);
|
||||
this.updateCurrentConfig();
|
||||
}
|
||||
|
||||
updateThreshold(event: Event): void {
|
||||
const value = parseInt((event.target as HTMLInputElement).value);
|
||||
this.threshold.set(value);
|
||||
this.updateCurrentConfig();
|
||||
}
|
||||
|
||||
updateDebounceTime(event: Event): void {
|
||||
const value = parseInt((event.target as HTMLInputElement).value);
|
||||
this.debounceTime.set(value);
|
||||
this.updateCurrentConfig();
|
||||
}
|
||||
|
||||
private updateCurrentConfig(): void {
|
||||
this.currentConfig.set({
|
||||
threshold: this.threshold(),
|
||||
debounceTime: this.debounceTime(),
|
||||
disabled: this.disableScroll()
|
||||
});
|
||||
}
|
||||
|
||||
// API methods - These would be called on component references
|
||||
scrollToTop(): void {
|
||||
console.log('Scroll to top - would call component method');
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
console.log('Scroll to bottom - would call component method');
|
||||
}
|
||||
|
||||
scrollToIndex(index: number): void {
|
||||
console.log(`Scroll to index ${index} - would call component method`);
|
||||
}
|
||||
|
||||
resetApiScroll(): void {
|
||||
console.log('Reset scroll - would call component method');
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
getTimeAgo(date: Date): string {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`;
|
||||
return `${Math.floor(minutes / 1440)}d ago`;
|
||||
}
|
||||
|
||||
private loadInitialData(): void {
|
||||
// Load initial data for all examples
|
||||
this.basicItems.set(this.generateItems(1, 20, 'basic'));
|
||||
this.templateItems.set(this.generateItems(1, 15, 'template'));
|
||||
this.bidirectionalItems.set(this.generateItems(1, 20, 'chat'));
|
||||
this.customItems.set(this.generateItems(1, 12, 'custom'));
|
||||
this.configItems.set(this.generateItems(1, 10, 'config'));
|
||||
}
|
||||
|
||||
private generateItems(startId: number, count: number, type: string): DemoItem[] {
|
||||
const categories = ['Technology', 'Science', 'Arts', 'Sports', 'Travel', 'Food', 'Music', 'Literature'];
|
||||
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: startId + i,
|
||||
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Item ${startId + i}`,
|
||||
description: `This is a sample description for ${type} item ${startId + i}. It contains some interesting content that demonstrates the infinite scroll functionality.`,
|
||||
timestamp: new Date(Date.now() - Math.random() * 86400000 * 30), // Random date within last 30 days
|
||||
category: categories[Math.floor(Math.random() * categories.length)]
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic" as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 100%;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 $semantic-spacing-component-lg 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;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: $semantic-spacing-component-xl 0 $semantic-spacing-component-md 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;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 $semantic-spacing-component-sm 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;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.event-log {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 $semantic-spacing-component-sm 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;
|
||||
}
|
||||
}
|
||||
|
||||
.events {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
// Scroll styling
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $semantic-color-border-primary $semantic-color-surface-container;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $semantic-color-surface-container;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
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);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { KanbanBoardComponent, KanbanColumn, KanbanItem, KanbanDragEvent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-kanban-board-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, KanbanBoardComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Kanban Board Layout Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Small</h4>
|
||||
<div style="height: 400px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="basicColumns()"
|
||||
size="sm"
|
||||
title="Small Kanban Board"
|
||||
(itemMoved)="onItemMoved($event)"
|
||||
(itemClicked)="onItemClicked($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Medium (Default)</h4>
|
||||
<div style="height: 400px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="basicColumns()"
|
||||
size="md"
|
||||
title="Medium Kanban Board"
|
||||
(itemMoved)="onItemMoved($event)"
|
||||
(itemClicked)="onItemClicked($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Large</h4>
|
||||
<div style="height: 400px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="basicColumns()"
|
||||
size="lg"
|
||||
title="Large Kanban Board"
|
||||
(itemMoved)="onItemMoved($event)"
|
||||
(itemClicked)="onItemClicked($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Full Featured Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Full Featured Kanban Board</h3>
|
||||
<div style="height: 600px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="fullColumns()"
|
||||
title="Project Management Board"
|
||||
subtitle="Drag and drop items between columns to update their status"
|
||||
size="md"
|
||||
(itemMoved)="onItemMoved($event)"
|
||||
(itemClicked)="onItemClicked($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Disabled</h4>
|
||||
<div style="height: 300px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="basicColumns()"
|
||||
[disabled]="true"
|
||||
title="Disabled Kanban Board">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Column with Limits</h4>
|
||||
<div style="height: 300px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="limitedColumns()"
|
||||
title="Limited Capacity Board"
|
||||
(itemMoved)="onItemMoved($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Events</h3>
|
||||
<div style="height: 500px; width: 100%;">
|
||||
<ui-kanban-board
|
||||
[columns]="interactiveColumns()"
|
||||
title="Interactive Kanban Board"
|
||||
subtitle="Click items or drag them around to see event logging below"
|
||||
(itemMoved)="onItemMoved($event)"
|
||||
(itemClicked)="onItemClicked($event)">
|
||||
</ui-kanban-board>
|
||||
</div>
|
||||
|
||||
<div class="event-log">
|
||||
<h4>Event Log:</h4>
|
||||
<div class="events">
|
||||
@for (event of eventLog(); track event.id) {
|
||||
<div class="event-item">
|
||||
<strong>{{ event.type }}:</strong> {{ event.message }}
|
||||
<small>{{ event.timestamp | date:'medium' }}</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './kanban-board-demo.component.scss'
|
||||
})
|
||||
export class KanbanBoardDemoComponent {
|
||||
eventLog = signal<Array<{ id: string; type: string; message: string; timestamp: Date }>>([]);
|
||||
|
||||
basicColumns(): KanbanColumn[] {
|
||||
return [
|
||||
{
|
||||
id: 'todo',
|
||||
title: 'To Do',
|
||||
items: [
|
||||
{ id: '1', title: 'Design homepage', description: 'Create wireframes and mockups' },
|
||||
{ id: '2', title: 'Setup database', priority: 'high' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'in-progress',
|
||||
title: 'In Progress',
|
||||
items: [
|
||||
{ id: '3', title: 'Build API endpoints', priority: 'medium', assignee: 'John' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
title: 'Done',
|
||||
items: [
|
||||
{ id: '4', title: 'Project setup', description: 'Initialize repository and dependencies' }
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
fullColumns(): KanbanColumn[] {
|
||||
return [
|
||||
{
|
||||
id: 'backlog',
|
||||
title: 'Backlog',
|
||||
items: [
|
||||
{
|
||||
id: 'item-1',
|
||||
title: 'User Authentication System',
|
||||
description: 'Implement login, registration, and password reset functionality',
|
||||
priority: 'high',
|
||||
tags: ['backend', 'security', 'user-management'],
|
||||
assignee: 'Sarah Connor',
|
||||
dueDate: new Date(2024, 11, 15)
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
title: 'Dashboard Analytics',
|
||||
description: 'Create comprehensive analytics dashboard with charts and metrics',
|
||||
priority: 'medium',
|
||||
tags: ['frontend', 'analytics', 'charts'],
|
||||
assignee: 'John Doe',
|
||||
dueDate: new Date(2024, 11, 20)
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
title: 'Mobile Responsiveness',
|
||||
description: 'Ensure all pages work correctly on mobile devices',
|
||||
priority: 'low',
|
||||
tags: ['frontend', 'responsive', 'css'],
|
||||
assignee: 'Jane Smith'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'todo',
|
||||
title: 'To Do',
|
||||
items: [
|
||||
{
|
||||
id: 'item-4',
|
||||
title: 'API Rate Limiting',
|
||||
description: 'Implement rate limiting for API endpoints to prevent abuse',
|
||||
priority: 'high',
|
||||
tags: ['backend', 'security', 'performance'],
|
||||
assignee: 'Bob Wilson',
|
||||
dueDate: new Date(2024, 11, 10)
|
||||
},
|
||||
{
|
||||
id: 'item-5',
|
||||
title: 'Email Notifications',
|
||||
description: 'Set up email notification system for important events',
|
||||
priority: 'medium',
|
||||
tags: ['backend', 'email', 'notifications'],
|
||||
assignee: 'Alice Brown'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'in-progress',
|
||||
title: 'In Progress',
|
||||
items: [
|
||||
{
|
||||
id: 'item-6',
|
||||
title: 'Database Optimization',
|
||||
description: 'Optimize database queries and add proper indexing',
|
||||
priority: 'high',
|
||||
tags: ['database', 'performance', 'optimization'],
|
||||
assignee: 'Mike Johnson',
|
||||
dueDate: new Date(2024, 11, 8)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: 'In Review',
|
||||
items: [
|
||||
{
|
||||
id: 'item-7',
|
||||
title: 'Unit Test Coverage',
|
||||
description: 'Increase unit test coverage to 90%',
|
||||
priority: 'medium',
|
||||
tags: ['testing', 'quality', 'coverage'],
|
||||
assignee: 'Lisa Davis',
|
||||
dueDate: new Date(2024, 11, 12)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
title: 'Done',
|
||||
items: [
|
||||
{
|
||||
id: 'item-8',
|
||||
title: 'Project Setup',
|
||||
description: 'Initialize project structure and dependencies',
|
||||
priority: 'high',
|
||||
tags: ['setup', 'infrastructure'],
|
||||
assignee: 'Team Lead'
|
||||
},
|
||||
{
|
||||
id: 'item-9',
|
||||
title: 'Design System',
|
||||
description: 'Create component library and design tokens',
|
||||
priority: 'medium',
|
||||
tags: ['design', 'components', 'ui'],
|
||||
assignee: 'Design Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
limitedColumns(): KanbanColumn[] {
|
||||
return [
|
||||
{
|
||||
id: 'todo',
|
||||
title: 'To Do',
|
||||
items: [
|
||||
{ id: '1', title: 'Task 1' },
|
||||
{ id: '2', title: 'Task 2' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'limited',
|
||||
title: 'Limited (Max 2)',
|
||||
maxItems: 2,
|
||||
items: [
|
||||
{ id: '3', title: 'Limited Task 1' },
|
||||
{ id: '4', title: 'Limited Task 2' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
title: 'Done',
|
||||
items: []
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
interactiveColumns(): KanbanColumn[] {
|
||||
return [
|
||||
{
|
||||
id: 'new',
|
||||
title: 'New',
|
||||
items: [
|
||||
{ id: 'i1', title: 'Interactive Item 1', description: 'Click me or drag me!' },
|
||||
{ id: 'i2', title: 'Interactive Item 2', priority: 'high', assignee: 'You' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
title: 'Active',
|
||||
items: [
|
||||
{ id: 'i3', title: 'Interactive Item 3', priority: 'medium' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
title: 'Complete',
|
||||
items: []
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
onItemMoved(event: KanbanDragEvent): void {
|
||||
this.logEvent('Item Moved',
|
||||
`"${event.item.title}" moved from "${event.fromColumn}" to "${event.toColumn}"`
|
||||
);
|
||||
|
||||
// In a real application, you would update your data here
|
||||
console.log('Item moved:', event);
|
||||
}
|
||||
|
||||
onItemClicked(event: { item: KanbanItem; columnId: string }): void {
|
||||
this.logEvent('Item Clicked',
|
||||
`"${event.item.title}" clicked in column "${event.columnId}"`
|
||||
);
|
||||
|
||||
console.log('Item clicked:', event);
|
||||
}
|
||||
|
||||
private logEvent(type: string, message: string): void {
|
||||
const currentLog = this.eventLog();
|
||||
const newEvent = {
|
||||
id: Date.now().toString(),
|
||||
type,
|
||||
message,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Keep only the last 10 events
|
||||
const updatedLog = [newEvent, ...currentLog].slice(0, 10);
|
||||
this.eventLog.set(updatedLog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.demo-container {
|
||||
.demo-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
|
||||
.demo-item {
|
||||
&.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MasonryComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-masonry-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MasonryComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Masonry Layout Demo</h2>
|
||||
<p>Pinterest-style dynamic grid layout that arranges items in columns with optimal vertical spacing.</p>
|
||||
|
||||
<!-- Column Count Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Count</h3>
|
||||
<div class="demo-row">
|
||||
@for (columns of columnCounts; track columns) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ columns }} Columns</h4>
|
||||
<ui-masonry [columns]="columns" gap="sm" padding="sm">
|
||||
@for (item of sampleItems.slice(0, 8); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto Responsive Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto Responsive</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Auto Fit (250px min width)</h4>
|
||||
<ui-masonry columns="auto-fit" gap="md" padding="md">
|
||||
@for (item of sampleItems.slice(0, 12); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Auto Fill (200px min width)</h4>
|
||||
<ui-masonry columns="auto-fill" gap="md" padding="md">
|
||||
@for (item of sampleItems.slice(0, 12); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-item">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-masonry [columns]="3" [gap]="gap" padding="sm">
|
||||
@for (item of sampleItems.slice(0, 6); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Padding Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Padding Options</h3>
|
||||
<div class="demo-row">
|
||||
@for (padding of paddings; track padding) {
|
||||
<div class="demo-item">
|
||||
<h4>Padding: {{ padding }}</h4>
|
||||
<ui-masonry [columns]="3" gap="sm" [padding]="padding">
|
||||
@for (item of sampleItems.slice(0, 6); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Alignment</h3>
|
||||
<div class="demo-row">
|
||||
@for (alignment of alignments; track alignment) {
|
||||
<div class="demo-item">
|
||||
<h4>Align: {{ alignment }}</h4>
|
||||
<ui-masonry [columns]="2" gap="md" padding="md" [alignment]="alignment">
|
||||
@for (item of sampleItems.slice(0, 4); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Item Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Item Variants</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Mixed Item Sizes</h4>
|
||||
<ui-masonry [columns]="3" gap="md" padding="md">
|
||||
<div class="ui-masonry-item ui-masonry-item--compact" style="height: 120px;">
|
||||
<h5>Compact Item</h5>
|
||||
<p>Compact padding for dense layouts</p>
|
||||
</div>
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable" style="height: 200px;">
|
||||
<h5>Comfortable Item</h5>
|
||||
<p>Standard comfortable padding for most use cases</p>
|
||||
</div>
|
||||
<div class="ui-masonry-item ui-masonry-item--spacious" style="height: 180px;">
|
||||
<h5>Spacious Item</h5>
|
||||
<p>Extra padding for premium content</p>
|
||||
</div>
|
||||
<div class="ui-masonry-item ui-masonry-item--flush" style="height: 160px;">
|
||||
<div style="padding: 16px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white;">
|
||||
<h5>Full Bleed Item</h5>
|
||||
<p>No padding for full-width content</p>
|
||||
</div>
|
||||
</div>
|
||||
@for (item of sampleItems.slice(0, 6); track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height">
|
||||
<h5>{{ item.title }}</h5>
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pinterest-style Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Pinterest-style Gallery</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item full-width">
|
||||
<ui-masonry columns="auto-fit" gap="lg" padding="lg" minColumnWidth="280px">
|
||||
@for (item of extendedItems; track item.id) {
|
||||
<div class="ui-masonry-item ui-masonry-item--comfortable"
|
||||
[style.height.px]="item.height"
|
||||
[style.background]="item.background">
|
||||
<h5 style="color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">{{ item.title }}</h5>
|
||||
<p style="color: rgba(255,255,255,0.9); text-shadow: 0 1px 2px rgba(0,0,0,0.3);">{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-masonry>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './masonry-demo.component.scss'
|
||||
})
|
||||
export class MasonryDemoComponent {
|
||||
columnCounts = [2, 3, 4] as const;
|
||||
gaps = ['sm', 'md', 'lg'] as const;
|
||||
paddings = ['none', 'sm', 'md', 'lg'] as const;
|
||||
alignments = ['start', 'center', 'end'] as const;
|
||||
|
||||
sampleItems = [
|
||||
{ id: 1, title: 'Card 1', content: 'Short content', height: 120 },
|
||||
{ id: 2, title: 'Card 2', content: 'Medium length content with more details', height: 180 },
|
||||
{ id: 3, title: 'Card 3', content: 'Brief', height: 100 },
|
||||
{ id: 4, title: 'Card 4', content: 'Longer content with multiple lines of text to demonstrate variable heights', height: 220 },
|
||||
{ id: 5, title: 'Card 5', content: 'Standard content', height: 140 },
|
||||
{ id: 6, title: 'Card 6', content: 'Extended content with even more text to show how masonry handles varying content lengths', height: 260 },
|
||||
{ id: 7, title: 'Card 7', content: 'Compact', height: 90 },
|
||||
{ id: 8, title: 'Card 8', content: 'Regular content block', height: 160 },
|
||||
{ id: 9, title: 'Card 9', content: 'Another piece of content', height: 130 },
|
||||
{ id: 10, title: 'Card 10', content: 'Different sized content', height: 200 },
|
||||
{ id: 11, title: 'Card 11', content: 'Small content', height: 110 },
|
||||
{ id: 12, title: 'Card 12', content: 'Large content with lots of text and information', height: 240 }
|
||||
];
|
||||
|
||||
extendedItems = [
|
||||
{ id: 1, title: 'Ocean Waves', content: 'Beautiful ocean scenery with crashing waves', height: 180, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
{ id: 2, title: 'Mountain Vista', content: 'Majestic mountain range at sunset', height: 220, background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
|
||||
{ id: 3, title: 'Forest Path', content: 'Winding path through dense forest', height: 160, background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
|
||||
{ id: 4, title: 'Desert Dunes', content: 'Golden sand dunes stretching to horizon', height: 200, background: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
|
||||
{ id: 5, title: 'City Skyline', content: 'Modern city with glowing lights', height: 140, background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
|
||||
{ id: 6, title: 'Northern Lights', content: 'Aurora borealis dancing across night sky', height: 250, background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
|
||||
{ id: 7, title: 'Tropical Beach', content: 'Palm trees and crystal clear water', height: 180, background: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' },
|
||||
{ id: 8, title: 'Snow Peaks', content: 'Snow-capped mountain peaks', height: 190, background: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)' },
|
||||
{ id: 9, title: 'Autumn Leaves', content: 'Colorful fall foliage', height: 170, background: 'linear-gradient(135deg, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%)' },
|
||||
{ id: 10, title: 'Starry Night', content: 'Countless stars in clear night sky', height: 210, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
{ id: 11, title: 'Wildflowers', content: 'Field of colorful wildflowers', height: 150, background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)' },
|
||||
{ id: 12, title: 'Waterfall', content: 'Powerful waterfall cascading down rocks', height: 230, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SelectComponent, SelectOption } from '../../../../../ui-essentials/src/lib/components/forms/select/select.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-select-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
SelectComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Select Component Showcase</h2>
|
||||
|
||||
<!-- Basic Select -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Select</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
|
||||
<ui-select
|
||||
label="Choose a country"
|
||||
placeholder="Select a country"
|
||||
[options]="countryOptions"
|
||||
[(ngModel)]="basicSelect1"
|
||||
(valueChange)="onSelectionChange('basic-country', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Choose a language"
|
||||
placeholder="Select a language"
|
||||
[options]="languageOptions"
|
||||
[(ngModel)]="basicSelect2"
|
||||
(valueChange)="onSelectionChange('basic-language', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Choose a framework"
|
||||
helperText="Select your preferred frontend framework"
|
||||
[options]="frameworkOptions"
|
||||
[(ngModel)]="basicSelect3"
|
||||
(valueChange)="onSelectionChange('basic-framework', $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; max-width: 400px;">
|
||||
<ui-select
|
||||
label="Small select"
|
||||
size="small"
|
||||
placeholder="Choose an option"
|
||||
[options]="sizeOptions"
|
||||
[(ngModel)]="sizeSmall"
|
||||
(valueChange)="onSelectionChange('size-small', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Medium select (default)"
|
||||
size="medium"
|
||||
placeholder="Choose an option"
|
||||
[options]="sizeOptions"
|
||||
[(ngModel)]="sizeMedium"
|
||||
(valueChange)="onSelectionChange('size-medium', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Large select"
|
||||
size="large"
|
||||
placeholder="Choose an option"
|
||||
[options]="sizeOptions"
|
||||
[(ngModel)]="sizeLarge"
|
||||
(valueChange)="onSelectionChange('size-large', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Style Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
|
||||
<ui-select
|
||||
label="Outlined variant (default)"
|
||||
variant="outlined"
|
||||
placeholder="Choose an option"
|
||||
[options]="variantOptions"
|
||||
[(ngModel)]="variantOutlined"
|
||||
(valueChange)="onSelectionChange('variant-outlined', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Filled variant"
|
||||
variant="filled"
|
||||
placeholder="Choose an option"
|
||||
[options]="variantOptions"
|
||||
[(ngModel)]="variantFilled"
|
||||
(valueChange)="onSelectionChange('variant-filled', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States and Validation -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States and Validation</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
|
||||
<ui-select
|
||||
label="Disabled select"
|
||||
[disabled]="true"
|
||||
placeholder="This is disabled"
|
||||
[options]="stateOptions"
|
||||
[(ngModel)]="stateDisabled"
|
||||
/>
|
||||
<ui-select
|
||||
label="Required field"
|
||||
[required]="true"
|
||||
placeholder="This field is required"
|
||||
[options]="stateOptions"
|
||||
[(ngModel)]="stateRequired"
|
||||
(valueChange)="onSelectionChange('state-required', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Field with error"
|
||||
placeholder="Select an option"
|
||||
errorText="This field has an error"
|
||||
[options]="stateOptions"
|
||||
[(ngModel)]="stateError"
|
||||
(valueChange)="onSelectionChange('state-error', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Field with helper text"
|
||||
placeholder="Select an option"
|
||||
helperText="This is helpful information about this field"
|
||||
[options]="stateOptions"
|
||||
[(ngModel)]="stateHelper"
|
||||
(valueChange)="onSelectionChange('state-helper', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Full Width -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Full Width</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<ui-select
|
||||
label="Full width select"
|
||||
placeholder="This select takes full width"
|
||||
[fullWidth]="true"
|
||||
[options]="fullWidthOptions"
|
||||
[(ngModel)]="fullWidthSelect"
|
||||
(valueChange)="onSelectionChange('full-width', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Complex Options -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Complex Options with Disabled Items</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; max-width: 400px;">
|
||||
<ui-select
|
||||
label="User roles"
|
||||
placeholder="Select a role"
|
||||
helperText="Some roles may be disabled based on permissions"
|
||||
[options]="roleOptions"
|
||||
[(ngModel)]="complexSelect1"
|
||||
(valueChange)="onSelectionChange('complex-roles', $event)"
|
||||
/>
|
||||
<ui-select
|
||||
label="Priority levels"
|
||||
placeholder="Select priority"
|
||||
[options]="priorityOptions"
|
||||
[(ngModel)]="complexSelect2"
|
||||
(valueChange)="onSelectionChange('complex-priority', $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)="setDefaultValues()"
|
||||
>
|
||||
Set Default Values
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="clearAllSelections()"
|
||||
>
|
||||
Clear 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-select
|
||||
label="Choose a country"
|
||||
placeholder="Select a country"
|
||||
[options]="countryOptions"
|
||||
[(ngModel)]="selectedCountry"
|
||||
(valueChange)="onCountryChange($event)">
|
||||
</ui-select></code></pre>
|
||||
|
||||
<h4>With Helper Text:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-select
|
||||
label="Framework"
|
||||
placeholder="Select framework"
|
||||
helperText="Choose your preferred frontend framework"
|
||||
size="large"
|
||||
variant="filled"
|
||||
[options]="frameworks"
|
||||
[(ngModel)]="selectedFramework">
|
||||
</ui-select></code></pre>
|
||||
|
||||
<h4>Required with Error State:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-select
|
||||
label="Required field"
|
||||
[required]="true"
|
||||
errorText="Please select an option"
|
||||
[options]="options"
|
||||
[(ngModel)]="selectedValue">
|
||||
</ui-select></code></pre>
|
||||
|
||||
<h4>Options Format:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>const options: SelectOption[] = [
|
||||
{{ '{' }} value: 'option1', label: 'Option 1' {{ '}' }},
|
||||
{{ '{' }} value: 'option2', label: 'Option 2' {{ '}' }},
|
||||
{{ '{' }} value: 'option3', label: 'Option 3 (Disabled)', disabled: true {{ '}' }}
|
||||
];</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 SelectDemoComponent {
|
||||
// Basic selects
|
||||
basicSelect1 = signal<string>('');
|
||||
basicSelect2 = signal<string>('en');
|
||||
basicSelect3 = signal<string>('');
|
||||
|
||||
// Size variants
|
||||
sizeSmall = signal<string>('');
|
||||
sizeMedium = signal<string>('medium');
|
||||
sizeLarge = signal<string>('');
|
||||
|
||||
// Style variants
|
||||
variantOutlined = signal<string>('');
|
||||
variantFilled = signal<string>('filled');
|
||||
|
||||
// States
|
||||
stateDisabled = signal<string>('disabled');
|
||||
stateRequired = signal<string>('');
|
||||
stateError = signal<string>('');
|
||||
stateHelper = signal<string>('');
|
||||
|
||||
// Full width
|
||||
fullWidthSelect = signal<string>('');
|
||||
|
||||
// Complex selects
|
||||
complexSelect1 = signal<string>('');
|
||||
complexSelect2 = signal<string>('medium');
|
||||
|
||||
// Control states
|
||||
globalDisabled = signal<boolean>(false);
|
||||
lastSelection = signal<string>('');
|
||||
|
||||
// Option arrays
|
||||
countryOptions: SelectOption[] = [
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
{ value: 'de', label: 'Germany' },
|
||||
{ value: 'fr', label: 'France' },
|
||||
{ value: 'jp', label: 'Japan' },
|
||||
{ value: 'au', label: 'Australia' }
|
||||
];
|
||||
|
||||
languageOptions: SelectOption[] = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'es', label: 'Spanish' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'de', label: 'German' },
|
||||
{ value: 'it', label: 'Italian' },
|
||||
{ value: 'pt', label: 'Portuguese' },
|
||||
{ value: 'zh', label: 'Chinese' }
|
||||
];
|
||||
|
||||
frameworkOptions: SelectOption[] = [
|
||||
{ value: 'angular', label: 'Angular' },
|
||||
{ value: 'react', label: 'React' },
|
||||
{ value: 'vue', label: 'Vue.js' },
|
||||
{ value: 'svelte', label: 'Svelte' },
|
||||
{ value: 'solid', label: 'SolidJS' }
|
||||
];
|
||||
|
||||
sizeOptions: SelectOption[] = [
|
||||
{ value: 'small', label: 'Small Size' },
|
||||
{ value: 'medium', label: 'Medium Size' },
|
||||
{ value: 'large', label: 'Large Size' }
|
||||
];
|
||||
|
||||
variantOptions: SelectOption[] = [
|
||||
{ value: 'outlined', label: 'Outlined Style' },
|
||||
{ value: 'filled', label: 'Filled Style' }
|
||||
];
|
||||
|
||||
stateOptions: SelectOption[] = [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
{ value: 'pending', label: 'Pending' }
|
||||
];
|
||||
|
||||
fullWidthOptions: SelectOption[] = [
|
||||
{ value: 'option1', label: 'This is a full width select option' },
|
||||
{ value: 'option2', label: 'Another full width option with longer text' },
|
||||
{ value: 'option3', label: 'Third option demonstrating full width behavior' }
|
||||
];
|
||||
|
||||
roleOptions: SelectOption[] = [
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'moderator', label: 'Moderator' },
|
||||
{ value: 'admin', label: 'Administrator' },
|
||||
{ value: 'super-admin', label: 'Super Administrator', disabled: true },
|
||||
{ value: 'guest', label: 'Guest', disabled: true }
|
||||
];
|
||||
|
||||
priorityOptions: SelectOption[] = [
|
||||
{ value: 'low', label: 'Low Priority' },
|
||||
{ value: 'medium', label: 'Medium Priority' },
|
||||
{ value: 'high', label: 'High Priority' },
|
||||
{ value: 'urgent', label: 'Urgent' },
|
||||
{ value: 'critical', label: 'Critical', disabled: true }
|
||||
];
|
||||
|
||||
onSelectionChange(selectId: string, value: any): void {
|
||||
const selectedOption = this.findOptionByValue(value);
|
||||
const displayValue = selectedOption ? selectedOption.label : value;
|
||||
this.lastSelection.set(`${selectId}: ${displayValue} (${value})`);
|
||||
console.log(`Select ${selectId} changed:`, value);
|
||||
}
|
||||
|
||||
private findOptionByValue(value: any): SelectOption | undefined {
|
||||
const allOptions = [
|
||||
...this.countryOptions,
|
||||
...this.languageOptions,
|
||||
...this.frameworkOptions,
|
||||
...this.sizeOptions,
|
||||
...this.variantOptions,
|
||||
...this.stateOptions,
|
||||
...this.fullWidthOptions,
|
||||
...this.roleOptions,
|
||||
...this.priorityOptions
|
||||
];
|
||||
return allOptions.find(option => option.value === value);
|
||||
}
|
||||
|
||||
setDefaultValues(): void {
|
||||
this.basicSelect1.set('us');
|
||||
this.basicSelect2.set('en');
|
||||
this.basicSelect3.set('angular');
|
||||
this.sizeSmall.set('small');
|
||||
this.sizeMedium.set('medium');
|
||||
this.sizeLarge.set('large');
|
||||
this.variantOutlined.set('outlined');
|
||||
this.variantFilled.set('filled');
|
||||
this.stateRequired.set('active');
|
||||
this.stateError.set('pending');
|
||||
this.stateHelper.set('inactive');
|
||||
this.fullWidthSelect.set('option2');
|
||||
this.complexSelect1.set('admin');
|
||||
this.complexSelect2.set('high');
|
||||
|
||||
this.lastSelection.set('All selects set to default values');
|
||||
}
|
||||
|
||||
clearAllSelections(): void {
|
||||
this.basicSelect1.set('');
|
||||
this.basicSelect2.set('');
|
||||
this.basicSelect3.set('');
|
||||
this.sizeSmall.set('');
|
||||
this.sizeMedium.set('');
|
||||
this.sizeLarge.set('');
|
||||
this.variantOutlined.set('');
|
||||
this.variantFilled.set('');
|
||||
this.stateRequired.set('');
|
||||
this.stateError.set('');
|
||||
this.stateHelper.set('');
|
||||
this.fullWidthSelect.set('');
|
||||
this.complexSelect1.set('');
|
||||
this.complexSelect2.set('');
|
||||
|
||||
this.lastSelection.set('All selections cleared');
|
||||
}
|
||||
|
||||
toggleDisabled(): void {
|
||||
this.globalDisabled.update(disabled => !disabled);
|
||||
this.lastSelection.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
getCurrentValues(): string {
|
||||
const values = {
|
||||
basic: {
|
||||
country: this.basicSelect1(),
|
||||
language: this.basicSelect2(),
|
||||
framework: this.basicSelect3()
|
||||
},
|
||||
sizes: {
|
||||
small: this.sizeSmall(),
|
||||
medium: this.sizeMedium(),
|
||||
large: this.sizeLarge()
|
||||
},
|
||||
variants: {
|
||||
outlined: this.variantOutlined(),
|
||||
filled: this.variantFilled()
|
||||
},
|
||||
states: {
|
||||
disabled: this.stateDisabled(),
|
||||
required: this.stateRequired(),
|
||||
error: this.stateError(),
|
||||
helper: this.stateHelper()
|
||||
},
|
||||
layout: {
|
||||
fullWidth: this.fullWidthSelect()
|
||||
},
|
||||
complex: {
|
||||
roles: this.complexSelect1(),
|
||||
priority: this.complexSelect2()
|
||||
},
|
||||
controls: {
|
||||
globalDisabled: this.globalDisabled(),
|
||||
lastSelection: this.lastSelection()
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
.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-component-md;
|
||||
}
|
||||
|
||||
.demo-split-container {
|
||||
height: 400px;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.demo-panel-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
height: 100%;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
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;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
|
||||
p {
|
||||
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;
|
||||
margin-bottom: $semantic-spacing-content-line-tight;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
|
||||
.demo-section .demo-split-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.demo-section .demo-controls {
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SplitViewComponent, PanelConfig, ResizeEvent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-split-view-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SplitViewComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Split View / Resizable Panels Demo</h2>
|
||||
|
||||
<!-- Basic Horizontal Split -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Horizontal Split</h3>
|
||||
<div class="demo-split-container">
|
||||
<ui-split-view
|
||||
[panels]="basicHorizontalPanels"
|
||||
direction="horizontal"
|
||||
size="md"
|
||||
(panelResized)="onPanelResized($event)">
|
||||
<div slot="left-panel" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Left Panel</h4>
|
||||
<p>This is the left panel content. Drag the divider to resize!</p>
|
||||
<p>You can add any content here including forms, lists, or other components.</p>
|
||||
</div>
|
||||
<div slot="right-panel" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Right Panel</h4>
|
||||
<p>This is the right panel content.</p>
|
||||
<p>Panels maintain their content during resize operations.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Usage:</strong> Drag the vertical divider to resize panels horizontally.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Basic Vertical Split -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Vertical Split</h3>
|
||||
<div class="demo-split-container">
|
||||
<ui-split-view
|
||||
[panels]="basicVerticalPanels"
|
||||
direction="vertical"
|
||||
size="md"
|
||||
(panelResized)="onPanelResized($event)">
|
||||
<div slot="top-panel" class="demo-panel-content demo-panel-content--elevated">
|
||||
<h4>Top Panel</h4>
|
||||
<p>This is the top panel in a vertical split layout.</p>
|
||||
</div>
|
||||
<div slot="bottom-panel" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Bottom Panel</h4>
|
||||
<p>This is the bottom panel. The divider can be dragged vertically.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Usage:</strong> Drag the horizontal divider to resize panels vertically.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Three Panel Layout -->
|
||||
<section class="demo-section">
|
||||
<h3>Three Panel Layout</h3>
|
||||
<div class="demo-split-container">
|
||||
<ui-split-view
|
||||
[panels]="threePanelLayout"
|
||||
direction="horizontal"
|
||||
size="lg"
|
||||
(panelResized)="onPanelResized($event)">
|
||||
<div slot="sidebar" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Sidebar</h4>
|
||||
<p>Navigation or menu content</p>
|
||||
</div>
|
||||
<div slot="main-content" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Main Content</h4>
|
||||
<p>Primary content area with the most space.</p>
|
||||
<p>This panel takes up the majority of the available space.</p>
|
||||
</div>
|
||||
<div slot="details-panel" class="demo-panel-content demo-panel-content--elevated">
|
||||
<h4>Details Panel</h4>
|
||||
<p>Additional information or tools</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-controls">
|
||||
<button (click)="collapsePanel('sidebar')">Collapse Sidebar</button>
|
||||
<button (click)="expandPanel('sidebar')" class="button--secondary">Expand Sidebar</button>
|
||||
<button (click)="collapsePanel('details-panel')" class="button--success">Collapse Details</button>
|
||||
<button (click)="expandPanel('details-panel')" class="button--success">Expand Details</button>
|
||||
<button (click)="resetThreePanelSizes()" class="button--danger">Reset Sizes</button>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Features:</strong> Multiple panels with programmatic collapse/expand controls.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
|
||||
<h4>Small Size</h4>
|
||||
<div class="demo-split-container" style="height: 200px;">
|
||||
<ui-split-view
|
||||
[panels]="sizeVariantPanels"
|
||||
direction="horizontal"
|
||||
size="sm">
|
||||
<div slot="size-left" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Small Split View</h4>
|
||||
<p>Compact layout with reduced spacing.</p>
|
||||
</div>
|
||||
<div slot="size-right" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Right Panel</h4>
|
||||
<p>Smaller padding and gaps.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
|
||||
<h4>Large Size</h4>
|
||||
<div class="demo-split-container" style="height: 500px;">
|
||||
<ui-split-view
|
||||
[panels]="sizeVariantPanels2"
|
||||
direction="horizontal"
|
||||
size="lg">
|
||||
<div slot="size-left-lg" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Large Split View</h4>
|
||||
<p>Spacious layout with generous spacing and padding.</p>
|
||||
<p>Perfect for complex layouts requiring more visual breathing room.</p>
|
||||
</div>
|
||||
<div slot="size-right-lg" class="demo-panel-content demo-panel-content--elevated">
|
||||
<h4>Right Panel</h4>
|
||||
<p>Enhanced spacing creates a more luxurious feel.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Sizes:</strong> sm (compact), md (default), lg (spacious) - affects padding and spacing.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Panel Constraints -->
|
||||
<section class="demo-section">
|
||||
<h3>Panel Size Constraints</h3>
|
||||
<div class="demo-split-container">
|
||||
<ui-split-view
|
||||
[panels]="constrainedPanels"
|
||||
direction="horizontal"
|
||||
size="md"
|
||||
(panelResized)="onPanelResized($event)">
|
||||
<div slot="constrained-left" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Constrained Panel</h4>
|
||||
<p>Min: 150px, Max: 400px</p>
|
||||
<p>Try resizing - this panel has size constraints!</p>
|
||||
</div>
|
||||
<div slot="constrained-right" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Flexible Panel</h4>
|
||||
<p>No constraints - adapts to available space.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Constraints:</strong> Left panel has min-width of 150px and max-width of 400px.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disabled State -->
|
||||
<section class="demo-section">
|
||||
<h3>Disabled State</h3>
|
||||
<div class="demo-split-container">
|
||||
<ui-split-view
|
||||
[panels]="basicHorizontalPanels"
|
||||
direction="horizontal"
|
||||
size="md"
|
||||
[disabled]="true">
|
||||
<div slot="left-panel" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Disabled Split View</h4>
|
||||
<p>Resizing is disabled - dividers cannot be dragged.</p>
|
||||
</div>
|
||||
<div slot="right-panel" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Static Panel</h4>
|
||||
<p>All interaction is disabled when the component is in disabled state.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Disabled:</strong> All resize functionality is disabled.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Nested Split Views -->
|
||||
<section class="demo-section">
|
||||
<h3>Nested Split Views</h3>
|
||||
<div class="demo-split-container" style="height: 500px;">
|
||||
<ui-split-view
|
||||
[panels]="outerNestedPanels"
|
||||
direction="horizontal"
|
||||
size="md">
|
||||
<div slot="outer-left" class="demo-panel-content demo-panel-content--secondary">
|
||||
<h4>Left Panel</h4>
|
||||
<p>This is the outer left panel.</p>
|
||||
</div>
|
||||
<div slot="outer-right" style="padding: 0;">
|
||||
<ui-split-view
|
||||
[panels]="innerNestedPanels"
|
||||
direction="vertical"
|
||||
size="md">
|
||||
<div slot="inner-top" class="demo-panel-content demo-panel-content--primary">
|
||||
<h4>Nested Top</h4>
|
||||
<p>This is a nested split view inside the right panel.</p>
|
||||
</div>
|
||||
<div slot="inner-bottom" class="demo-panel-content demo-panel-content--elevated">
|
||||
<h4>Nested Bottom</h4>
|
||||
<p>Fully functional nested resizable panels.</p>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
</ui-split-view>
|
||||
</div>
|
||||
<div class="demo-info">
|
||||
<p><strong>Nesting:</strong> Split views can be nested to create complex layouts.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Information -->
|
||||
<section class="demo-section">
|
||||
<h3>Resize Events</h3>
|
||||
<div class="demo-info">
|
||||
<p><strong>Last Resize Event:</strong> {{ lastResizeEvent || 'None' }}</p>
|
||||
<p><strong>Resize Count:</strong> {{ resizeCount }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './split-view-demo.component.scss'
|
||||
})
|
||||
export class SplitViewDemoComponent {
|
||||
resizeCount = 0;
|
||||
lastResizeEvent = '';
|
||||
|
||||
// Basic horizontal panels
|
||||
basicHorizontalPanels: PanelConfig[] = [
|
||||
{ id: 'left-panel', size: '40%' },
|
||||
{ id: 'right-panel', size: '60%' }
|
||||
];
|
||||
|
||||
// Basic vertical panels
|
||||
basicVerticalPanels: PanelConfig[] = [
|
||||
{ id: 'top-panel', size: '30%' },
|
||||
{ id: 'bottom-panel', size: '70%' }
|
||||
];
|
||||
|
||||
// Three panel layout
|
||||
threePanelLayout: PanelConfig[] = [
|
||||
{ id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
|
||||
{ id: 'main-content', size: '1fr', variant: 'primary' },
|
||||
{ id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
|
||||
];
|
||||
|
||||
// Size variant panels
|
||||
sizeVariantPanels: PanelConfig[] = [
|
||||
{ id: 'size-left', size: '50%' },
|
||||
{ id: 'size-right', size: '50%' }
|
||||
];
|
||||
|
||||
sizeVariantPanels2: PanelConfig[] = [
|
||||
{ id: 'size-left-lg', size: '40%' },
|
||||
{ id: 'size-right-lg', size: '60%' }
|
||||
];
|
||||
|
||||
// Constrained panels
|
||||
constrainedPanels: PanelConfig[] = [
|
||||
{ id: 'constrained-left', size: '300px', minSize: 150, maxSize: 400 },
|
||||
{ id: 'constrained-right', size: '1fr' }
|
||||
];
|
||||
|
||||
// Nested panels
|
||||
outerNestedPanels: PanelConfig[] = [
|
||||
{ id: 'outer-left', size: '30%' },
|
||||
{ id: 'outer-right', size: '70%' }
|
||||
];
|
||||
|
||||
innerNestedPanels: PanelConfig[] = [
|
||||
{ id: 'inner-top', size: '40%' },
|
||||
{ id: 'inner-bottom', size: '60%' }
|
||||
];
|
||||
|
||||
onPanelResized(event: ResizeEvent): void {
|
||||
this.resizeCount++;
|
||||
this.lastResizeEvent = `Panel ${event.panelId} resized to ${Math.round(event.newSize)}px (${event.direction})`;
|
||||
console.log('Panel resized:', event);
|
||||
}
|
||||
|
||||
collapsePanel(panelId: string): void {
|
||||
const panel = this.threePanelLayout.find(p => p.id === panelId);
|
||||
if (panel) {
|
||||
panel.collapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
expandPanel(panelId: string): void {
|
||||
const panel = this.threePanelLayout.find(p => p.id === panelId);
|
||||
if (panel) {
|
||||
panel.collapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
resetThreePanelSizes(): void {
|
||||
this.threePanelLayout = [
|
||||
{ id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
|
||||
{ id: 'main-content', size: '1fr', variant: 'primary' },
|
||||
{ id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-scroll-container {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
background: linear-gradient(to bottom, #fafafa, #f0f0f0);
|
||||
}
|
||||
|
||||
.demo-content-spacer {
|
||||
padding: 2rem;
|
||||
min-height: 600px;
|
||||
|
||||
p {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-filler {
|
||||
height: 100px;
|
||||
margin: 1rem 0;
|
||||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1rem 2rem;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-content-small {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.demo-variant-example {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
|
||||
h4 {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 1rem;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-effect-example {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
|
||||
h4 {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 1rem;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
|
||||
select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-preview {
|
||||
height: 300px;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
background: linear-gradient(45deg, #f9f9f9 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #f9f9f9 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #f9f9f9 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #f9f9f9 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
min-width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { StickyLayoutComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-sticky-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, StickyLayoutComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Sticky Layout Demo</h2>
|
||||
<p>Scroll down to see the sticky positioning in action.</p>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Position Variants</h3>
|
||||
<div class="demo-scroll-container">
|
||||
<ui-sticky-layout position="top" variant="header" [shadow]="true">
|
||||
<div class="demo-content">Sticky Top Header</div>
|
||||
</ui-sticky-layout>
|
||||
|
||||
<div class="demo-content-spacer">
|
||||
<p>Scroll down to see sticky positioning...</p>
|
||||
<div class="demo-filler">Content area 1</div>
|
||||
<div class="demo-filler">Content area 2</div>
|
||||
<div class="demo-filler">Content area 3</div>
|
||||
<div class="demo-filler">Content area 4</div>
|
||||
<div class="demo-filler">Content area 5</div>
|
||||
</div>
|
||||
|
||||
<ui-sticky-layout position="bottom" variant="footer" [shadow]="true">
|
||||
<div class="demo-content">Sticky Bottom Footer</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variant Styles</h3>
|
||||
<div class="demo-grid">
|
||||
@for (variant of variants; track variant.name) {
|
||||
<div class="demo-variant-example">
|
||||
<h4>{{ variant.name }}</h4>
|
||||
<ui-sticky-layout
|
||||
[position]="variant.position"
|
||||
[variant]="variant.variant"
|
||||
[shadow]="variant.shadow"
|
||||
[background]="variant.background">
|
||||
<div class="demo-content-small">
|
||||
{{ variant.name }} Sticky
|
||||
</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Offset Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Offset Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (offset of offsets; track offset) {
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
[offset]="offset"
|
||||
variant="toolbar"
|
||||
[shadow]="true">
|
||||
<div class="demo-content-small">
|
||||
Offset: {{ offset }}
|
||||
</div>
|
||||
</ui-sticky-layout>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Background and Effects -->
|
||||
<section class="demo-section">
|
||||
<h3>Background & Effects</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-effect-example">
|
||||
<h4>Blur Effect</h4>
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
background="surface"
|
||||
[blur]="true"
|
||||
[shadow]="true">
|
||||
<div class="demo-content-small">Blurred Background</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-effect-example">
|
||||
<h4>Backdrop</h4>
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
background="backdrop"
|
||||
[border]="true">
|
||||
<div class="demo-content-small">Backdrop Filter</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-effect-example">
|
||||
<h4>Floating</h4>
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
variant="floating"
|
||||
offset="md">
|
||||
<div class="demo-content-small">Floating Style</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-effect-example">
|
||||
<h4>Toolbar</h4>
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
variant="toolbar"
|
||||
[shadow]="true"
|
||||
[border]="true">
|
||||
<div class="demo-content-small">Toolbar Style</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<ui-sticky-layout
|
||||
position="top"
|
||||
[size]="size"
|
||||
variant="header"
|
||||
[shadow]="true">
|
||||
<div class="demo-content-small">
|
||||
Size: {{ size }}
|
||||
</div>
|
||||
</ui-sticky-layout>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Position:
|
||||
<select [(ngModel)]="selectedPosition" (change)="updateConfig()">
|
||||
@for (pos of positions; track pos) {
|
||||
<option [value]="pos">{{ pos }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="selectedVariant" (change)="updateConfig()">
|
||||
@for (variant of variantOptions; track variant) {
|
||||
<option [value]="variant">{{ variant }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="enableShadow" (change)="updateConfig()">
|
||||
Shadow
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="enableBorder" (change)="updateConfig()">
|
||||
Border
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="enableBlur" (change)="updateConfig()">
|
||||
Blur
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-preview">
|
||||
<ui-sticky-layout
|
||||
[position]="selectedPosition"
|
||||
[variant]="selectedVariant"
|
||||
[shadow]="enableShadow"
|
||||
[border]="enableBorder"
|
||||
[blur]="enableBlur"
|
||||
background="surface">
|
||||
<div class="demo-content">
|
||||
Dynamic Sticky Layout
|
||||
</div>
|
||||
</ui-sticky-layout>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './sticky-layout-demo.component.scss'
|
||||
})
|
||||
export class StickyLayoutDemoComponent {
|
||||
positions = ['top', 'bottom', 'left', 'right'] as const;
|
||||
offsets = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
sizes = ['auto', 'full', 'content'] as const;
|
||||
variantOptions = ['default', 'header', 'sidebar', 'footer', 'floating', 'toolbar'] as const;
|
||||
|
||||
variants = [
|
||||
{ name: 'Header', variant: 'header' as const, position: 'top' as const, shadow: true, background: 'surface' as const },
|
||||
{ name: 'Sidebar', variant: 'sidebar' as const, position: 'left' as const, shadow: false, background: 'surface-secondary' as const },
|
||||
{ name: 'Footer', variant: 'footer' as const, position: 'bottom' as const, shadow: true, background: 'surface' as const },
|
||||
{ name: 'Floating', variant: 'floating' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const },
|
||||
{ name: 'Toolbar', variant: 'toolbar' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const }
|
||||
];
|
||||
|
||||
// Interactive controls
|
||||
selectedPosition: typeof this.positions[number] = 'top';
|
||||
selectedVariant: typeof this.variantOptions[number] = 'header';
|
||||
enableShadow = true;
|
||||
enableBorder = false;
|
||||
enableBlur = false;
|
||||
|
||||
updateConfig(): void {
|
||||
console.log('Sticky layout config updated:', {
|
||||
position: this.selectedPosition,
|
||||
variant: this.selectedVariant,
|
||||
shadow: this.enableShadow,
|
||||
border: this.enableBorder,
|
||||
blur: this.enableBlur
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/textarea.component';
|
||||
import { faComment, faEdit, faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-textarea-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TextareaComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Textarea Component Showcase</h2>
|
||||
|
||||
<!-- Basic Textarea -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Textarea</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="Basic textarea"
|
||||
placeholder="Enter your message here..."
|
||||
[(ngModel)]="basicTextarea1"
|
||||
(textareaChange)="onTextareaChange('basic-1', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Textarea with helper text"
|
||||
placeholder="Share your thoughts..."
|
||||
helperText="Your feedback is important to us"
|
||||
[(ngModel)]="basicTextarea2"
|
||||
(textareaChange)="onTextareaChange('basic-2', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Pre-filled textarea"
|
||||
placeholder="Enter description..."
|
||||
[(ngModel)]="basicTextarea3"
|
||||
(textareaChange)="onTextareaChange('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-textarea
|
||||
label="Small textarea"
|
||||
size="sm"
|
||||
placeholder="Small size textarea..."
|
||||
[(ngModel)]="sizeSmall"
|
||||
(textareaChange)="onTextareaChange('size-small', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Medium textarea (default)"
|
||||
size="md"
|
||||
placeholder="Medium size textarea..."
|
||||
[(ngModel)]="sizeMedium"
|
||||
(textareaChange)="onTextareaChange('size-medium', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Large textarea"
|
||||
size="lg"
|
||||
placeholder="Large size textarea..."
|
||||
[(ngModel)]="sizeLarge"
|
||||
(textareaChange)="onTextareaChange('size-large', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Style Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="Outlined variant (default)"
|
||||
variant="outlined"
|
||||
placeholder="Outlined style textarea..."
|
||||
[(ngModel)]="variantOutlined"
|
||||
(textareaChange)="onTextareaChange('variant-outlined', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Filled variant"
|
||||
variant="filled"
|
||||
placeholder="Filled style textarea..."
|
||||
[(ngModel)]="variantFilled"
|
||||
(textareaChange)="onTextareaChange('variant-filled', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Underlined variant"
|
||||
variant="underlined"
|
||||
placeholder="Underlined style textarea..."
|
||||
[(ngModel)]="variantUnderlined"
|
||||
(textareaChange)="onTextareaChange('variant-underlined', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States and Validation</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="Default state"
|
||||
state="default"
|
||||
placeholder="Normal textarea..."
|
||||
[(ngModel)]="stateDefault"
|
||||
(textareaChange)="onTextareaChange('state-default', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Success state"
|
||||
state="success"
|
||||
placeholder="Successful input..."
|
||||
helperText="This looks good!"
|
||||
[(ngModel)]="stateSuccess"
|
||||
(textareaChange)="onTextareaChange('state-success', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Warning state"
|
||||
state="warning"
|
||||
placeholder="Input with warning..."
|
||||
helperText="Please double-check this content"
|
||||
[(ngModel)]="stateWarning"
|
||||
(textareaChange)="onTextareaChange('state-warning', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Error state"
|
||||
state="error"
|
||||
placeholder="Input with error..."
|
||||
errorMessage="This field contains an error"
|
||||
[(ngModel)]="stateError"
|
||||
(textareaChange)="onTextareaChange('state-error', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive States</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="Disabled textarea"
|
||||
[disabled]="true"
|
||||
placeholder="This textarea is disabled"
|
||||
[(ngModel)]="stateDisabled"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Readonly textarea"
|
||||
[readonly]="true"
|
||||
placeholder="This textarea is readonly"
|
||||
[(ngModel)]="stateReadonly"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Required field"
|
||||
[required]="true"
|
||||
placeholder="This field is required"
|
||||
[(ngModel)]="stateRequired"
|
||||
(textareaChange)="onTextareaChange('state-required', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Loading state"
|
||||
[loading]="true"
|
||||
placeholder="This textarea is in loading state"
|
||||
[(ngModel)]="stateLoading"
|
||||
(textareaChange)="onTextareaChange('state-loading', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Character Limits and Counters -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Character Limits and Counters</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="Textarea with character limit"
|
||||
placeholder="You have 100 characters max..."
|
||||
[maxLength]="100"
|
||||
[showCharacterCount]="true"
|
||||
helperText="Please keep your message concise"
|
||||
[(ngModel)]="charLimit1"
|
||||
(textareaChange)="onTextareaChange('char-limit-1', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Tweet-style limit"
|
||||
placeholder="What's happening?"
|
||||
[maxLength]="280"
|
||||
[showCharacterCount]="true"
|
||||
[(ngModel)]="charLimit2"
|
||||
(textareaChange)="onTextareaChange('char-limit-2', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Long form content"
|
||||
placeholder="Write your article here..."
|
||||
[maxLength]="1000"
|
||||
[showCharacterCount]="true"
|
||||
[rows]="8"
|
||||
[(ngModel)]="charLimit3"
|
||||
(textareaChange)="onTextareaChange('char-limit-3', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Row and Resize Options -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Rows and Resize Options</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="2 rows (compact)"
|
||||
placeholder="Short input area..."
|
||||
[rows]="2"
|
||||
resize="none"
|
||||
[(ngModel)]="rowsCompact"
|
||||
(textareaChange)="onTextareaChange('rows-compact', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="6 rows (expanded)"
|
||||
placeholder="Larger input area..."
|
||||
[rows]="6"
|
||||
resize="vertical"
|
||||
[(ngModel)]="rowsExpanded"
|
||||
(textareaChange)="onTextareaChange('rows-expanded', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Auto-resize textarea"
|
||||
placeholder="This textarea will grow as you type..."
|
||||
[autoResize]="true"
|
||||
[rows]="3"
|
||||
[(ngModel)]="autoResizeTextarea"
|
||||
(textareaChange)="onTextareaChange('auto-resize', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Horizontal resize"
|
||||
placeholder="You can resize this horizontally..."
|
||||
resize="horizontal"
|
||||
[(ngModel)]="horizontalResize"
|
||||
(textareaChange)="onTextareaChange('horizontal-resize', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Both directions resize"
|
||||
placeholder="Resize in any direction..."
|
||||
resize="both"
|
||||
[(ngModel)]="bothResize"
|
||||
(textareaChange)="onTextareaChange('both-resize', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Icons and Clearable -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Icons and Clearable Features</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-textarea
|
||||
label="With prefix icon"
|
||||
placeholder="Type your comment..."
|
||||
[prefixIcon]="faComment"
|
||||
[(ngModel)]="iconPrefix"
|
||||
(textareaChange)="onTextareaChange('icon-prefix', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="With suffix icon"
|
||||
placeholder="Write your email content..."
|
||||
[suffixIcon]="faEnvelope"
|
||||
[(ngModel)]="iconSuffix"
|
||||
(textareaChange)="onTextareaChange('icon-suffix', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Clearable textarea"
|
||||
placeholder="You can clear this easily..."
|
||||
[clearable]="true"
|
||||
[prefixIcon]="faEdit"
|
||||
[(ngModel)]="clearableTextarea"
|
||||
(textareaChange)="onTextareaChange('clearable', $event)"
|
||||
(clear)="onClear('clearable')"
|
||||
/>
|
||||
</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)="fillSampleText()"
|
||||
>
|
||||
Fill Sample Text
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="clearAllTextareas()"
|
||||
>
|
||||
Clear 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 (lastChange()) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last change:</strong> {{ lastChange() }}
|
||||
</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-textarea
|
||||
label="Message"
|
||||
placeholder="Enter your message..."
|
||||
[(ngModel)]="message"
|
||||
(textareaChange)="onMessageChange($event)">
|
||||
</ui-textarea></code></pre>
|
||||
|
||||
<h4>With Character Limit:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-textarea
|
||||
label="Tweet"
|
||||
placeholder="What's happening?"
|
||||
[maxLength]="280"
|
||||
[showCharacterCount]="true"
|
||||
size="lg"
|
||||
[(ngModel)]="tweetText">
|
||||
</ui-textarea></code></pre>
|
||||
|
||||
<h4>Auto-resize with Validation:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-textarea
|
||||
label="Feedback"
|
||||
[required]="true"
|
||||
[autoResize]="true"
|
||||
[clearable]="true"
|
||||
state="error"
|
||||
errorMessage="Please provide feedback"
|
||||
[prefixIcon]="faComment"
|
||||
[(ngModel)]="feedback">
|
||||
</ui-textarea></code></pre>
|
||||
|
||||
<h4>Resize Options:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Resize options:
|
||||
resize="none" // No resize
|
||||
resize="vertical" // Vertical only (default)
|
||||
resize="horizontal" // Horizontal only
|
||||
resize="both" // Both directions</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 TextareaDemoComponent {
|
||||
// FontAwesome icons
|
||||
readonly faComment = faComment;
|
||||
readonly faEdit = faEdit;
|
||||
readonly faEnvelope = faEnvelope;
|
||||
|
||||
// Basic textareas
|
||||
basicTextarea1 = signal<string>('');
|
||||
basicTextarea2 = signal<string>('');
|
||||
basicTextarea3 = signal<string>('This is pre-filled content that demonstrates how the textarea looks with existing text.');
|
||||
|
||||
// Size variants
|
||||
sizeSmall = signal<string>('');
|
||||
sizeMedium = signal<string>('');
|
||||
sizeLarge = signal<string>('');
|
||||
|
||||
// Style variants
|
||||
variantOutlined = signal<string>('');
|
||||
variantFilled = signal<string>('');
|
||||
variantUnderlined = signal<string>('');
|
||||
|
||||
// States
|
||||
stateDefault = signal<string>('');
|
||||
stateSuccess = signal<string>('Great job on completing this form!');
|
||||
stateWarning = signal<string>('Please review this content carefully.');
|
||||
stateError = signal<string>('This content has some issues.');
|
||||
|
||||
// Interactive states
|
||||
stateDisabled = signal<string>('This content cannot be edited');
|
||||
stateReadonly = signal<string>('This is read-only information that you can view but not modify');
|
||||
stateRequired = signal<string>('');
|
||||
stateLoading = signal<string>('');
|
||||
|
||||
// Character limits
|
||||
charLimit1 = signal<string>('');
|
||||
charLimit2 = signal<string>('');
|
||||
charLimit3 = signal<string>('');
|
||||
|
||||
// Rows and resize
|
||||
rowsCompact = signal<string>('');
|
||||
rowsExpanded = signal<string>('');
|
||||
autoResizeTextarea = signal<string>('Start typing and watch this textarea grow automatically as you add more content...');
|
||||
horizontalResize = signal<string>('');
|
||||
bothResize = signal<string>('');
|
||||
|
||||
// Icons and clearable
|
||||
iconPrefix = signal<string>('');
|
||||
iconSuffix = signal<string>('');
|
||||
clearableTextarea = signal<string>('You can clear this text by clicking the X button');
|
||||
|
||||
// Control states
|
||||
globalDisabled = signal<boolean>(false);
|
||||
lastChange = signal<string>('');
|
||||
|
||||
onTextareaChange(textareaId: string, value: string): void {
|
||||
const preview = value.length > 50 ? value.substring(0, 50) + '...' : value;
|
||||
this.lastChange.set(`${textareaId}: "${preview}" (${value.length} chars)`);
|
||||
console.log(`Textarea ${textareaId} changed:`, value);
|
||||
}
|
||||
|
||||
onClear(textareaId: string): void {
|
||||
this.lastChange.set(`${textareaId}: cleared`);
|
||||
console.log(`Textarea ${textareaId} cleared`);
|
||||
}
|
||||
|
||||
fillSampleText(): void {
|
||||
const sampleTexts = [
|
||||
'This is sample text for demonstration purposes.',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
'Here is some example content to show how the textarea handles different amounts of text.',
|
||||
'Sample content with multiple lines.\nSecond line of content.\nThird line for testing.',
|
||||
'Short text',
|
||||
'A longer piece of sample text that demonstrates how the textarea component handles various lengths of content and provides a good example for testing purposes.'
|
||||
];
|
||||
|
||||
this.basicTextarea1.set(sampleTexts[0]);
|
||||
this.basicTextarea2.set(sampleTexts[1]);
|
||||
this.sizeSmall.set(sampleTexts[4]);
|
||||
this.sizeMedium.set(sampleTexts[2]);
|
||||
this.sizeLarge.set(sampleTexts[5]);
|
||||
this.variantOutlined.set(sampleTexts[0]);
|
||||
this.variantFilled.set(sampleTexts[1]);
|
||||
this.variantUnderlined.set(sampleTexts[2]);
|
||||
this.stateDefault.set(sampleTexts[3]);
|
||||
this.stateRequired.set(sampleTexts[0]);
|
||||
this.stateLoading.set(sampleTexts[1]);
|
||||
this.charLimit1.set('Sample text within limit');
|
||||
this.charLimit2.set('This is a tweet-style message that fits within the character limit.');
|
||||
this.charLimit3.set(sampleTexts[5]);
|
||||
this.rowsCompact.set('Compact');
|
||||
this.rowsExpanded.set(sampleTexts[3]);
|
||||
this.iconPrefix.set('Comment with icon');
|
||||
this.iconSuffix.set('Email content here');
|
||||
this.clearableTextarea.set('This text can be cleared');
|
||||
|
||||
this.lastChange.set('All textareas filled with sample text');
|
||||
}
|
||||
|
||||
clearAllTextareas(): void {
|
||||
this.basicTextarea1.set('');
|
||||
this.basicTextarea2.set('');
|
||||
this.sizeSmall.set('');
|
||||
this.sizeMedium.set('');
|
||||
this.sizeLarge.set('');
|
||||
this.variantOutlined.set('');
|
||||
this.variantFilled.set('');
|
||||
this.variantUnderlined.set('');
|
||||
this.stateDefault.set('');
|
||||
this.stateRequired.set('');
|
||||
this.stateLoading.set('');
|
||||
this.charLimit1.set('');
|
||||
this.charLimit2.set('');
|
||||
this.charLimit3.set('');
|
||||
this.rowsCompact.set('');
|
||||
this.rowsExpanded.set('');
|
||||
this.autoResizeTextarea.set('');
|
||||
this.horizontalResize.set('');
|
||||
this.bothResize.set('');
|
||||
this.iconPrefix.set('');
|
||||
this.iconSuffix.set('');
|
||||
this.clearableTextarea.set('');
|
||||
|
||||
this.lastChange.set('All textareas cleared');
|
||||
}
|
||||
|
||||
toggleDisabled(): void {
|
||||
this.globalDisabled.update(disabled => !disabled);
|
||||
this.lastChange.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
getCurrentValues(): string {
|
||||
const values = {
|
||||
basic: {
|
||||
textarea1: this.basicTextarea1(),
|
||||
textarea2: this.basicTextarea2(),
|
||||
textarea3: this.basicTextarea3()
|
||||
},
|
||||
sizes: {
|
||||
small: this.sizeSmall(),
|
||||
medium: this.sizeMedium(),
|
||||
large: this.sizeLarge()
|
||||
},
|
||||
variants: {
|
||||
outlined: this.variantOutlined(),
|
||||
filled: this.variantFilled(),
|
||||
underlined: this.variantUnderlined()
|
||||
},
|
||||
states: {
|
||||
default: this.stateDefault(),
|
||||
success: this.stateSuccess(),
|
||||
warning: this.stateWarning(),
|
||||
error: this.stateError(),
|
||||
disabled: this.stateDisabled(),
|
||||
readonly: this.stateReadonly(),
|
||||
required: this.stateRequired(),
|
||||
loading: this.stateLoading()
|
||||
},
|
||||
characterLimits: {
|
||||
limit100: this.charLimit1(),
|
||||
limit280: this.charLimit2(),
|
||||
limit1000: this.charLimit3()
|
||||
},
|
||||
rowsAndResize: {
|
||||
compact: this.rowsCompact(),
|
||||
expanded: this.rowsExpanded(),
|
||||
autoResize: this.autoResizeTextarea(),
|
||||
horizontal: this.horizontalResize(),
|
||||
both: this.bothResize()
|
||||
},
|
||||
iconsAndClearable: {
|
||||
prefix: this.iconPrefix(),
|
||||
suffix: this.iconSuffix(),
|
||||
clearable: this.clearableTextarea()
|
||||
},
|
||||
controls: {
|
||||
globalDisabled: this.globalDisabled(),
|
||||
lastChange: this.lastChange()
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
|
||||
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
|
||||
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart, faCircleDot, faColumns,
|
||||
faFolderOpen, faDesktop, faListUl
|
||||
faFolderOpen, faDesktop, faListUl, faArrowDown, faThumbtack
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { DemoRoutes } from '../../demos';
|
||||
import { ScrollContainerComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
@@ -115,6 +115,8 @@ export class DashboardComponent {
|
||||
faDesktop = faDesktop;
|
||||
faNewspaper = faNewspaper;
|
||||
faListUl = faListUl;
|
||||
faArrowDown = faArrowDown;
|
||||
faThumbtack = faThumbtack;
|
||||
|
||||
menuItems: any = []
|
||||
|
||||
@@ -235,15 +237,21 @@ export class DashboardComponent {
|
||||
this.createChildItem("container", "Container", this.faBoxOpen),
|
||||
this.createChildItem("feed-layout", "Feed Layout", this.faNewspaper),
|
||||
this.createChildItem("flex", "Flex (Flexbox Container)", this.faArrowsAlt),
|
||||
this.createChildItem("gallery-grid", "Gallery Grid", this.faImages),
|
||||
this.createChildItem("grid-system", "Grid System", this.faGripVertical),
|
||||
this.createChildItem("grid-container", "Grid Container", this.faThLarge),
|
||||
this.createChildItem("infinite-scroll-container", "Infinite Scroll Container", this.faArrowDown),
|
||||
this.createChildItem("kanban-board", "Kanban Board Layout", this.faColumns),
|
||||
this.createChildItem("list-detail-layout", "List Detail Layout", this.faListUl),
|
||||
this.createChildItem("masonry", "Masonry Layout", this.faThLarge),
|
||||
this.createChildItem("scroll-container", "Scroll Container", this.faListAlt),
|
||||
this.createChildItem("supporting-pane-layout", "Supporting Pane Layout", this.faWindowMaximize),
|
||||
this.createChildItem("section", "Section (Semantic Wrapper)", this.faLayerGroup),
|
||||
this.createChildItem("sidebar-layout", "Sidebar Layout", this.faBars),
|
||||
this.createChildItem("spacer", "Spacer", this.faArrowsAlt),
|
||||
this.createChildItem("split-view", "Split View / Resizable Panels", this.faColumns),
|
||||
this.createChildItem("stack", "Stack (VStack/HStack)", this.faStream),
|
||||
this.createChildItem("sticky-layout", "Sticky Layout", this.faThumbtack),
|
||||
this.createChildItem("tabs-container", "Tabs Container", this.faFolderOpen),
|
||||
this.createChildItem("dashboard-shell", "Dashboard Shell", this.faDesktop)
|
||||
];
|
||||
@@ -263,8 +271,18 @@ export class DashboardComponent {
|
||||
|
||||
// Utilities (standalone)
|
||||
this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
|
||||
this.addMenuItem("accessibility", "Accessability", this.faCheckSquare);
|
||||
this.addMenuItem("animations", "Animations", this.faCheckSquare);
|
||||
this.addMenuItem("hcl-studio", "HCL Themes", this.faCheckSquare);
|
||||
this.addMenuItem("font-manager", "Font Manager", this.faCheckSquare);
|
||||
this.addMenuItem("data-utils", "Data Utils", this.faCheckSquare);
|
||||
this.addMenuItem("code-display", "Code Display", this.faCheckSquare);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
menuClick(id: string) {
|
||||
console.log(JSON.stringify(id))
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
.children-container {
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid semantic.$semantic-color-border-secondary;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
::ng-deep .child-menu-item {
|
||||
|
||||
@@ -11,9 +11,19 @@ $comfortaa-font-path: "/fonts/comfortaa/" !default;
|
||||
// Import project variables (which now has semantic tokens available)
|
||||
@import 'scss/_variables';
|
||||
|
||||
// Import UI Animations
|
||||
@import '../../ui-animations/src/styles/index';
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: 16px; // Base font size for rem calculations
|
||||
|
||||
// Define default CSS custom properties for fonts
|
||||
// These can be overridden by the font manager
|
||||
--font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-family-mono: ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--font-family-display: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -23,8 +33,8 @@ body {
|
||||
background-color: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Apply base typography using semantic tokens
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
// Apply base typography using CSS custom properties first, then fallback to semantic tokens
|
||||
font-family: var(--font-family-sans, 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);
|
||||
@@ -32,4 +42,37 @@ body {
|
||||
// Improve text rendering
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
// Support font transitions
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// Global typography classes that respect font variables
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-family-display, var(--font-family-sans));
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// Serif elements
|
||||
blockquote, .serif {
|
||||
font-family: var(--font-family-serif);
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// Monospace elements
|
||||
code, pre, kbd, samp, .mono {
|
||||
font-family: var(--font-family-mono);
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// Sans-serif elements
|
||||
.sans {
|
||||
font-family: var(--font-family-sans);
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// Display elements
|
||||
.display {
|
||||
font-family: var(--font-family-display);
|
||||
transition: font-family 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user