@pecb-ui/components
v1.1.12
Published
Professional Angular UI Components Library with TypeScript and SCSS by PECB
Readme
@pecb-ui/components
A professional Angular 17 UI components library built with TypeScript and SCSS. Features Storybook for component development and documentation, full accessibility compliance (WCAG 2.1 AA), and tree-shakeable, AOT-compatible builds.
Features
- 🎯 Angular 17.3+ - Built with the latest Angular features
- 📦 APF Compliant - Angular Package Format for maximum compatibility
- 🌳 Tree-shakeable - Only import what you use
- ⚡ AOT Compatible - Optimized for Ahead-of-Time compilation
- ♿ WCAG 2.1 AA - Full accessibility compliance
- 🎨 SCSS Design System - Professional variables, mixins, and utilities
- 📚 Storybook - Interactive component documentation
- 🔒 Strict TypeScript - Full type safety
Installation
From npm (recommended)
npm install @pecb-ui/componentsFrom local package
npm install /path/to/dist/ui-components/pecb-ui-components-1.0.0.tgzPeer Dependencies
This library requires the following peer dependencies:
{
"@angular/common": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/animations": "^17.3.0"
}Quick Start
1. Import the Module
// app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { PecbComponentsModule } from "@pecb-ui/components";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
PecbComponentsModule, // Import the PECB UI components module
],
bootstrap: [AppComponent],
})
export class AppModule {}2. Import Global Styles (Optional)
If you want to use the PECB design system variables in your application:
// styles.scss
@import "@pecb-ui/components/styles/main";Or import specific abstracts:
// In your component SCSS
@import "@pecb-ui/components/styles/abstracts/variables";
@import "@pecb-ui/components/styles/abstracts/mixins";
.my-component {
color: $primary-color;
padding: $spacing-md;
@include elevation(2);
}3. Use Components
<!-- app.component.html -->
<pecb-button variant="primary" size="medium" (clicked)="handleClick()"> Click Me </pecb-button>
<pecb-card elevation="md">
<pecb-card-header>
<h3>Welcome</h3>
</pecb-card-header>
<pecb-card-body>
<p>Your content goes here</p>
</pecb-card-body>
</pecb-card>Components
Button Component
A versatile button component with multiple variants, sizes, and accessibility features.
<!-- Basic button -->
<pecb-button variant="primary" (clicked)="onClick()"> Submit </pecb-button>
<!-- With loading state -->
<pecb-button variant="primary" [loading]="isLoading"> Save </pecb-button>
<!-- Disabled button -->
<pecb-button variant="danger" [disabled]="true"> Delete </pecb-button>
<!-- Full width button -->
<pecb-button variant="success" [fullWidth]="true"> Continue </pecb-button>
<!-- Icon button with accessibility label -->
<pecb-button variant="secondary" ariaLabel="Close dialog" iconLeft="✕"> </pecb-button>
<!-- Toggle button -->
<pecb-button variant="secondary" [ariaPressed]="isActive" (clicked)="toggle()"> Toggle </pecb-button>
<!-- Button that controls a dropdown -->
<pecb-button variant="primary" [ariaExpanded]="isOpen" ariaHasPopup="menu" ariaControls="dropdown-menu" (clicked)="toggleDropdown()"> Menu </pecb-button>Properties:
| Property | Type | Default | Description |
| -------------- | ------------------------------------------------------------------------------------ | ----------- | ----------------------------------- |
| variant | 'primary' \| 'secondary' \| 'success' \| 'danger' \| 'warning' \| 'info' \| 'text' | 'primary' | Visual style variant |
| size | 'small' \| 'medium' \| 'large' | 'medium' | Button size |
| disabled | boolean | false | Disable the button |
| loading | boolean | false | Show loading spinner |
| fullWidth | boolean | false | Make button full width |
| type | 'button' \| 'submit' \| 'reset' | 'button' | HTML button type |
| iconLeft | string | - | Icon before text |
| iconRight | string | - | Icon after text |
| ariaLabel | string | - | Accessible label |
| ariaExpanded | boolean | - | ARIA expanded state |
| ariaPressed | boolean | - | ARIA pressed state (toggle buttons) |
| ariaHasPopup | boolean \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'dialog' | - | Popup type |
| ariaControls | string | - | ID of controlled element |
Events:
| Event | Type | Description |
| --------- | -------------------------- | ----------------------- |
| clicked | EventEmitter<MouseEvent> | Emitted on button click |
Card Component
A container component for displaying content with optional header, body, and footer sections.
<!-- Basic card -->
<pecb-card elevation="md">
<pecb-card-header>
<h3>Card Title</h3>
</pecb-card-header>
<pecb-card-body>
<p>Card content goes here</p>
</pecb-card-body>
<pecb-card-footer>
<pecb-button variant="primary">Action</pecb-button>
</pecb-card-footer>
</pecb-card>
<!-- Hoverable card -->
<pecb-card elevation="sm" [hoverable]="true">
<pecb-card-body>
<p>Hover over me!</p>
</pecb-card-body>
</pecb-card>
<!-- Card as article (accessible) -->
<pecb-card role="article" ariaLabelledBy="article-title">
<pecb-card-header>
<h2 id="article-title">Article Headline</h2>
</pecb-card-header>
<pecb-card-body>
<p>Article content...</p>
</pecb-card-body>
</pecb-card>
<!-- Card as region -->
<pecb-card role="region" ariaLabel="Product Information">
<pecb-card-body>
<p>Product details...</p>
</pecb-card-body>
</pecb-card>Properties:
| Property | Type | Default | Description |
| ----------------- | ---------------------------------------------------------- | -------- | ------------------------ |
| elevation | 'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Shadow depth |
| hoverable | boolean | false | Enable hover effect |
| noPadding | boolean | false | Remove default padding |
| role | 'article' \| 'region' \| 'group' \| 'listitem' \| 'none' | 'none' | Semantic role |
| ariaLabel | string | - | Accessible label |
| ariaLabelledBy | string | - | ID of labelling element |
| ariaDescribedBy | string | - | ID of describing element |
Video Player Component
The PECB course video player — a fully custom-skinned, accessible HTML5 <video> wrapper
that reproduces the Course Player — Content Redesign stage: rounded dark 16:9 surface,
poster, brand watermark + section label, centre play button and a bottom control bar
(seek bar, play/pause, rewind, volume, time, captions, playback-speed settings, fullscreen).
It is standalone (no icon-registry or service dependencies), responsive through container queries (it adapts to the width of its slot, not the viewport), keyboard operable, touch friendly and themable through CSS custom properties — so it can be dropped into any Angular 17+ project.
import { VideoPlayerComponent } from "@pecb-ui/components";
// or: import { VideoPlayerComponent } from '@pecb-ui/components/data-display';<!-- Minimal -->
<pecb-video-player src="https://cdn.example.com/lesson.mp4" poster="https://cdn.example.com/lesson.jpg" title="Introduction to management systems — Part 3" label="SECTION 2"> </pecb-video-player>
<!-- Multiple sources + captions -->
<pecb-video-player [src]="[{ src: 'lesson.webm', type: 'video/webm' }, { src: 'lesson.mp4', type: 'video/mp4' }]" [tracks]="[{ src: 'lesson.en.vtt', srclang: 'en', label: 'English', default: true }]" crossOrigin="anonymous" label="SECTION 2"> </pecb-video-player>
<!-- Driven from outside (transcript / lesson list) with two-way bindings -->
<pecb-video-player [src]="lesson.src" [(playing)]="playing" [(currentTime)]="at" [(captionsEnabled)]="captions" (playbackEnded)="markLessonComplete()" (timeUpdate)="saveProgress($event.currentTime)"> </pecb-video-player>
<!-- Project custom overlays (quiz, end-screen…) on top of the stage -->
<pecb-video-player [src]="src">
<div pecbVideoOverlay class="my-quiz-overlay">…</div>
</pecb-video-player>Properties:
| Property | Type | Default | Description |
| ---------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------ | ---------------------------------------------------------- |
| src | string \| VideoPlayerSource[] | – | Media URL or list of <source> candidates |
| poster | string | – | Poster image |
| tracks | VideoPlayerTrack[] | [] | Caption/subtitle tracks (CC button appears when non-empty) |
| title | string | – | Accessible title (part of the region label) |
| crossOrigin | 'anonymous' \| 'use-credentials' | – | Needed for cross-origin caption files |
| preload | 'none' \| 'metadata' \| 'auto' | 'metadata' | Preload hint |
| autoplay / loop / playsInline | boolean | false / false / true | Native media flags |
| startTime | number | 0 | Initial position in seconds |
| watermark | string | 'PECB' | Bottom-left brand mark ('' hides it) |
| label | string | – | Bottom-right label, e.g. SECTION 2 |
| aspectRatio | string | '16 / 9' | CSS aspect ratio of the stage |
| seekStep | number | 10 | Seconds for rewind button / arrow keys |
| playbackRates | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | Entries of the speed menu |
| showCenterButton, showSkipBack, showVolume, showTime, showSettings, showFullscreen | boolean | true | Toggle individual controls |
| showCaptions | boolean \| undefined | undefined | Force the CC button on/off (auto by default) |
| labels | Partial<VideoPlayerLabels> | {} | Override user-visible strings (i18n) |
Two-way models ([(…)]): playing, currentTime, volume, muted, playbackRate, captionsEnabled.
Outputs: timeUpdate, loadedMetadata, seekEnd (VideoPlayerTimeEvent), playbackEnded,
fullscreenChange (boolean), playerError (VideoPlayerError) — plus the …Change output of every model.
Public methods: play(), pause(), togglePlay(), seek(s), seekBy(Δs), skipBack(),
toggleMute(), setVolume(v), setPlaybackRate(r), toggleCaptions(), toggleFullscreen().
Keyboard: Space/K play-pause · ←/→ or J/L seek ±seekStep · ↑/↓ volume ·
M mute · C captions · F fullscreen · Home/End · Esc closes the speed menu.
The seek bar is a role="slider" with arrow/Page/Home/End support.
Theming (CSS custom properties on the host):
pecb-video-player {
--pecb-video-radius: 12px; /* stage corner radius */
--pecb-video-bg: #0b0b0d; /* stage background */
--pecb-video-accent: #fff; /* progress fill, knob, slider */
--pecb-video-controls-color: rgba(255, 255, 255, 0.92);
--pecb-video-font-display: "Plus Jakarta Sans", sans-serif;
--pecb-video-font-body: "Inter", sans-serif;
--pecb-video-focus-ring: #fff;
}Course Content Panel Component
The Content sidebar from the Course Player — Content Redesign — the list that sits beside the course video. It reproduces the design exactly: Content / Transcript tabs, lesson search, the collapsible Section accordion (one section open at a time, auto-opening the section that owns the active lesson), status discs (completed · now playing · in-progress ring · not started · locked), quiz rows with badge and score, per-lesson progress bars, the connected "Part 1 / Part 2 …" rail for multi-part lessons, and a searchable transcript with click-to-seek, live cue highlighting and an autoscroll toggle.
It is standalone and data-driven — no player dependency — and sizes itself with container queries, so it works in a 408px sidebar, a drawer or a phone-width column.
import { CourseContentPanelComponent } from "@pecb-ui/components";<pecb-course-content-panel [modules]="modules" [transcript]="cues" [(activeLessonId)]="lessonId" [playing]="playing()" [currentTime]="currentTime()" (lessonSelect)="openLesson($event)" (transcriptSeek)="player.seek($event)"> </pecb-course-content-panel>const modules: CourseModule[] = [
{
id: "m2",
title: "Introduction to management systems",
lessons: [
// A "Base — Part n" title makes the panel group consecutive lessons under one
// sub-heading and list them as Part 1, Part 2, … on a connected rail.
{ id: "l2", title: "Introduction … — Part 1", durationSeconds: 1204, status: "completed" },
{ id: "l3", title: "Introduction … — Part 2", durationSeconds: 925, status: "completed" },
{ id: "l4", title: "Introduction … — Part 3", durationSeconds: 612, status: "playing", resumeSeconds: 440, src: "lesson-3.mp4", poster: "lesson-3.jpg", stageLabel: "SECTION 2" },
{ id: "q1", type: "quiz", title: "Introduction …", questionCount: 5, correctCount: 4, status: "completed" },
],
},
];Properties:
| Property | Type | Default | Description |
| ------------------------------------------------------------------ | ----------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
| modules | CourseModule[] | [] | Course structure |
| transcript | CourseTranscriptCue[] | [] | Fallback cues (a lesson's own transcript wins) |
| playing | boolean | false | Live playback state — drives the "now playing" row |
| currentTime | number | – | Live playhead: highlights the transcript cue and draws live progress on the active row |
| layout | 'grouped' \| 'timeline' | 'grouped' | Section accordion, or one flat connected timeline |
| density | 'comfortable' \| 'compact' | 'comfortable' | Row spacing |
| accent | 'charcoal' \| 'red' | 'charcoal' | Colour of the "now playing" treatment |
| showTabs, showSearch, showTranscriptSearch, showAutoscroll | boolean | true | Toggle chrome |
| showProgress | boolean | false | Circular course-progress header + certificate pill |
| showCertificate | boolean | true | The pill inside that header |
| soloSingleLessonModules | boolean | false | Render one-lesson sections as direct-play rows |
| labels | Partial<CourseContentPanelLabels> | {} | Override user-visible strings (i18n) |
Two-way models ([(…)]): activeLessonId, tab, openModuleId, autoscroll, query, transcriptQuery.
Outputs: lessonSelect (CourseLesson), transcriptSeek (seconds), moduleToggle
({ module, open }), certificateClick.
Lesson status: completed · playing · started · available · locked. Only the active
lesson can render as "now playing", and a locked lesson is disabled and not selectable.
Accessibility: tabs are a real tablist (arrow keys switch), section headers are
aria-expanded buttons with arrow/Home/End navigation, and collapsed sections are inert, so their
lessons stay out of the tab order.
Course Player Component
The video stage with the Content sidebar attached — the full design composition, ready to drop in.
It wires pecb-video-player and pecb-course-content-panel together:
picking a lesson loads its media and resumes where it left off, clicking a transcript cue seeks the
video, and the sidebar follows playback live. Below ~900px of available width the two columns
collapse into a stacked layout (container queries — it also works inside a drawer or split view).
import { CoursePlayerComponent } from "@pecb-ui/components";<pecb-course-player [modules]="modules" [(activeLessonId)]="lessonId" [(playing)]="playing" [(currentTime)]="at" (lessonSelect)="track($event)" (lessonEnded)="markComplete($event)" (timeUpdate)="saveProgress($event.currentTime)"> </pecb-course-player>Properties: modules, transcript, plus the video options src, poster, tracks,
stageLabel (all used as fallbacks when the active lesson doesn't carry its own), watermark,
aspectRatio, preload, crossOrigin, seekStep, videoLabels, autoPlayOnSelect,
showLessonTitle; and the panel options panelPosition ('end' | 'start'), layout, density,
accent, showTabs, showSearch, showProgress, soloSingleLessonModules, panelLabels.
Two-way models: activeLessonId, playing, currentTime, panelTab.
Outputs: lessonSelect, lessonEnded, timeUpdate, certificateClick, playerError.
Layout hooks:
pecb-course-player {
--pecb-course-player-panel-width: 408px; /* side column width */
--pecb-course-player-gap: 20px; /* column gap */
--pecb-course-player-panel-stacked-height: 460px; /* panel height once stacked */
--pecb-course-player-radius: 12px; /* panel corner radius */
}Side by side, the sidebar takes its height from the video stage and scrolls internally — give the player a height (or let the stage define it) and the columns stay aligned. Once stacked, the player grows to fit, so don't pin it to a fixed height in that range.
Theming the panel (both components):
pecb-course-content-panel {
--pecb-course-panel-accent: #a11e29; /* "now playing" colour */
--pecb-course-panel-bg: #fff;
--pecb-course-panel-border: #ececec;
--pecb-course-panel-font-display: "Plus Jakarta Sans", sans-serif;
--pecb-course-panel-font-body: "Inter", sans-serif;
}Services
NotificationService
A stateless, injectable service for displaying toast notifications.
import { NotificationService } from '@pecb-ui/components';
@Component({...})
export class MyComponent {
constructor(private notificationService: NotificationService) {}
showSuccess(): void {
this.notificationService.success('Operation completed successfully!');
}
showError(): void {
this.notificationService.error('An error occurred', 'Error');
}
showWarning(): void {
this.notificationService.warning('Please review your input', 'Warning');
}
showInfo(): void {
this.notificationService.info('New updates available');
}
showCustom(): void {
this.notificationService.show({
message: 'Custom notification',
type: 'info',
duration: 10000,
position: 'bottom-right',
dismissible: true
});
}
}ThemeService
A stateless, injectable service for managing application themes.
import { ThemeService } from '@pecb-ui/components';
@Component({...})
export class MyComponent {
isDarkMode$ = this.themeService.theme$.pipe(
map(config => config.resolvedTheme === 'dark')
);
constructor(private themeService: ThemeService) {}
toggleTheme(): void {
this.themeService.toggleTheme();
}
setDarkMode(): void {
this.themeService.setTheme('dark');
}
useSystemPreference(): void {
this.themeService.useSystemTheme();
}
setCustomColors(): void {
this.themeService.setCustomVariables({
primaryColor: '#007bff',
backgroundColor: '#f8f9fa'
});
}
}LoadingService
A stateless, injectable service for managing loading states.
import { LoadingService } from '@pecb-ui/components';
@Component({...})
export class MyComponent {
isLoading$ = this.loadingService.isLoading$('data-fetch');
constructor(private loadingService: LoadingService) {}
async loadData(): Promise<void> {
this.loadingService.start('data-fetch', 'Loading data...');
try {
const data = await this.fetchData();
// Process data
} finally {
this.loadingService.stop('data-fetch');
}
}
// Or use the helper method
async loadDataAlt(): Promise<void> {
const data = await this.loadingService.withLoading(
'data-fetch',
() => this.fetchData(),
'Loading data...'
);
}
}Utilities
The library exports utility functions for common operations:
import {
// String utilities
toKebabCase,
toCamelCase,
truncate,
slugify,
// DOM utilities
generateUniqueId,
getFocusableElements,
trapFocus,
copyToClipboard,
// Accessibility utilities
announceToScreenReader,
getButtonAriaAttributes,
prefersReducedMotion,
// Error handling
createError,
wrapError,
tryAsync,
} from "@pecb-ui/components";Types & Interfaces
import {
// Common types
Size,
ComponentSize,
ColorVariant,
Position,
// Component interfaces
Disableable,
Loadable,
Focusable,
SelectableItem,
MenuItem,
// Service interfaces
NotificationConfig,
ThemeConfig,
LoadingState,
} from "@pecb-ui/components";SCSS Design System
Variables
// Colors
$primary-color: #1976d2;
$accent-color: #ff4081;
$success-color: #4caf50;
$warn-color: #f44336;
$info-color: #2196f3;
// Spacing (8px base unit)
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
// Typography
$font-family-base:
"Inter",
-apple-system,
BlinkMacSystemFont,
sans-serif;
$font-size-sm: 0.875rem;
$font-size-md: 1rem;
$font-size-lg: 1.125rem;
// Border radius
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 12px;Mixins
// Responsive breakpoints
@include respond-to("md") {
// Styles for medium screens and up
}
// Flexbox utilities
@include flex-center;
@include flex-between;
// Elevation (shadows)
@include elevation(2);
// Focus visible
@include focus-visible {
outline: 2px solid $primary-color;
}
// Transitions
@include transition(background-color, color);Development
Prerequisites
- Node.js 18+
- npm 9+
- Angular CLI 17.3+
Setup
# Clone the repository
git clone https://github.com/pecb-ui/components.git
cd components
# Install dependencies
npm install
# Build the library
npm run build
# Run Storybook
npm run storybook
# Run tests
npm testBuilding
# Development build
npm run build
# Production build
npm run build:prod
# Create npm package
npm run publish:libTesting
# Run tests in watch mode
npm test
# Run tests once with coverage
npm run test:ciStorybook
# Start Storybook dev server
npm run storybook
# Build Storybook for deployment
npm run build-storybookBrowser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Roadmap
Planned components:
- Input/Form controls
- Modal/Dialog
- Dropdown/Select
- Tabs
- Accordion
- Table
- Pagination
- Badge
- Avatar
- Tooltip
- Alert/Toast
- Progress indicators
- Date picker
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes using Conventional Commits
- Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see the LICENSE file for details.
Support
For issues and questions, please use the GitHub issue tracker.
