Initial commit: kanban-elements-demo app

Interactive Angular 19 demo for @sda/kanban-elements-ui with board
view, pipeline view, swimlanes, drag-and-drop, WIP limits, custom
card templates, and dark mode toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Giuliano Silvestro
2026-02-13 21:57:35 +10:00
commit 10982976bf
15 changed files with 15043 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
dist/
node_modules/
.angular/
*.tgz

76
angular.json Normal file
View File

@@ -0,0 +1,76 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"kanban-elements-demo": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"standalone": true,
"changeDetection": "OnPush"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/kanban-elements-demo",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [],
"styles": [
"src/styles.scss"
],
"scripts": [],
"preserveSymlinks": true
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1MB",
"maximumError": "2MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "10kB",
"maximumError": "20kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "kanban-elements-demo:build:production"
},
"development": {
"buildTarget": "kanban-elements-demo:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}

13887
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "kanban-elements-demo",
"version": "0.0.1",
"description": "Demo application for @sda/kanban-elements-ui library",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"serve": "ng serve --open"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.2.0",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"@sda/kanban-elements-ui": "file:../kanban-elements-ui/dist",
"@sda/base-ui": "file:../../sda-frontend/libs/base-ui/dist",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.0",
"@angular/cli": "^19.2.0",
"@angular/compiler-cli": "^19.2.0",
"typescript": "~5.7.2"
}
}

214
src/app/app.component.html Normal file
View File

@@ -0,0 +1,214 @@
<div class="demo" [class.dark]="darkMode()">
<!-- Header -->
<header class="demo__header">
<div class="demo__header-content">
<div>
<h1 class="demo__title">Kanban Elements UI</h1>
<p class="demo__subtitle">Interactive demo for &#64;sda/kanban-elements-ui</p>
</div>
<button class="demo__theme-toggle" (click)="toggleDarkMode()">
{{ darkMode() ? 'Light Mode' : 'Dark Mode' }}
</button>
</div>
</header>
<!-- Navigation -->
<nav class="demo__nav">
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'basic'"
(click)="setSection('basic')"
>Basic Board</button>
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'pipeline'"
(click)="setSection('pipeline')"
>Pipeline View</button>
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'swimlane'"
(click)="setSection('swimlane')"
>Swimlane Board</button>
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'wip'"
(click)="setSection('wip')"
>WIP Limits</button>
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'custom'"
(click)="setSection('custom')"
>Custom Templates</button>
<button
class="demo__nav-btn"
[class.demo__nav-btn--active]="activeSection() === 'full'"
(click)="setSection('full')"
>Full-Featured</button>
</nav>
<!-- Event log -->
@if (lastEvent()) {
<div class="demo__event-log">
Last event: <strong>{{ lastEvent() }}</strong>
</div>
}
<!-- Content -->
<main class="demo__content">
<!-- Basic Board -->
@if (activeSection() === 'basic') {
<section class="demo__section">
<h2 class="demo__section-title">Basic Board</h2>
<p class="demo__section-desc">Drag cards between columns. Click to interact. Use the "+" button to add new cards.</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="basicCards()"
[columns]="columns"
[allowQuickAdd]="true"
[allowColumnReorder]="true"
(cardMove)="onCardMove($event)"
(cardClick)="onCardClick($event)"
(cardAdd)="onCardAdd($event)"
(columnToggle)="onColumnToggle($event)"
(columnReorder)="onColumnReorder($event)"
/>
</div>
</section>
}
<!-- Pipeline View -->
@if (activeSection() === 'pipeline') {
<section class="demo__section">
<h2 class="demo__section-title">Pipeline View</h2>
<p class="demo__section-desc">Same data displayed as a pipeline with arrow connectors between stages.</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="basicCards()"
[columns]="columns"
[viewMode]="pipelineView()"
[allowQuickAdd]="false"
(cardMove)="onCardMove($event)"
(cardClick)="onCardClick($event)"
/>
</div>
</section>
}
<!-- Swimlane Board -->
@if (activeSection() === 'swimlane') {
<section class="demo__section">
<h2 class="demo__section-title">Swimlane Board</h2>
<p class="demo__section-desc">Cards grouped into swimlanes by priority. Collapse/expand swimlanes by clicking their headers.</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="swimlaneCards()"
[columns]="columns"
[swimlanes]="swimlanes"
[allowQuickAdd]="true"
(cardMove)="onSwimlaneCardMove($event)"
(cardClick)="onCardClick($event)"
(cardAdd)="onCardAdd($event)"
(swimlaneToggle)="onSwimlaneToggle($event)"
/>
</div>
</section>
}
<!-- WIP Limits -->
@if (activeSection() === 'wip') {
<section class="demo__section">
<h2 class="demo__section-title">WIP Limits</h2>
<p class="demo__section-desc">Columns have WIP limits. "In Progress" is at capacity (warning), "Review" exceeds its limit (exceeded state). Try dragging more cards in!</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="wipCards()"
[columns]="wipColumns"
[allowQuickAdd]="false"
(cardMove)="onWipCardMove($event)"
(cardClick)="onCardClick($event)"
(wipExceeded)="onWipExceeded($event)"
/>
</div>
</section>
}
<!-- Custom Templates -->
@if (activeSection() === 'custom') {
<section class="demo__section">
<h2 class="demo__section-title">Custom Templates</h2>
<p class="demo__section-desc">Using kbCardDef to render CRM-style deal cards with custom layout.</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="basicCards()"
[columns]="columns"
[allowQuickAdd]="false"
(cardMove)="onCardMove($event)"
(cardClick)="onCardClick($event)"
>
<ng-template kbCardDef let-card>
<div
class="custom-card"
draggable="true"
>
<div class="custom-card__header">
<span class="custom-card__type" [attr.data-type]="$any(card).type || 'task'">
{{ $any(card).type || 'task' }}
</span>
@if (card.storyPoints) {
<span class="custom-card__points">{{ card.storyPoints }}pt</span>
}
</div>
<div class="custom-card__title">{{ card.title }}</div>
@if (card.assignees?.length) {
<div class="custom-card__assignees">
@for (a of card.assignees; track a.id) {
<span class="custom-card__assignee">{{ a.name }}</span>
}
</div>
}
@if (card.labels?.length) {
<div class="custom-card__labels">
@for (l of card.labels; track l.id) {
<span class="custom-card__label" [style.color]="l.color">{{ l.text }}</span>
}
</div>
}
</div>
</ng-template>
</kb-board>
</div>
</section>
}
<!-- Full-Featured -->
@if (activeSection() === 'full') {
<section class="demo__section">
<h2 class="demo__section-title">Full-Featured</h2>
<p class="demo__section-desc">Everything combined: toolbar with search, view toggle, sort, filters, and all interactions.</p>
<div class="demo__board-wrapper">
<kb-board
[cards]="fullCards()"
[columns]="columns"
[viewMode]="fullViewMode()"
[showToolbar]="true"
[searchable]="true"
[showViewToggle]="true"
[allowQuickAdd]="true"
[allowColumnReorder]="true"
(cardMove)="onFullCardMove($event)"
(cardClick)="onCardClick($event)"
(cardAdd)="onFullCardAdd($event)"
(columnToggle)="onColumnToggle($event)"
(columnReorder)="onColumnReorder($event)"
(viewChange)="onViewChange($event)"
(sortChange)="onSortChange($event)"
(filterChange)="onFilterChange($event)"
(wipExceeded)="onWipExceeded($event)"
/>
</div>
</section>
}
</main>
</div>

235
src/app/app.component.scss Normal file
View File

@@ -0,0 +1,235 @@
.demo {
min-height: 100vh;
background: var(--color-bg-primary);
color: var(--color-text-primary);
}
// Header
.demo__header {
position: sticky;
top: 0;
z-index: 50;
background: var(--kb-glass-bg, rgba(255, 255, 255, 0.72));
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--color-border-primary);
padding: 1.25rem 2.5rem;
}
.demo__header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.demo__title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.demo__subtitle {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.demo__theme-toggle {
padding: 0.5rem 1.25rem;
border: 1px solid var(--color-border-primary);
border-radius: 999px;
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
&:hover {
background: var(--color-bg-hover);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
}
// Navigation
.demo__nav {
display: flex;
gap: 0.375rem;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
padding: 0.5rem 2.5rem;
max-width: 1400px;
margin: 0 auto;
overflow-x: auto;
}
.demo__nav-btn {
padding: 0.5rem 1.125rem;
border: none;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
border-radius: 999px;
transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
&:hover {
color: var(--color-text-primary);
background: var(--color-bg-hover);
}
&--active {
color: var(--color-primary-500);
background: var(--color-primary-50);
}
}
// Event log
.demo__event-log {
padding: 0.625rem 2.5rem;
background: var(--kb-glass-bg, rgba(255, 255, 255, 0.72));
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
font-size: 0.8125rem;
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border-primary);
strong {
color: var(--color-primary-500);
}
}
// Content
.demo__content {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 2.5rem;
}
.demo__section {
// Full width for boards
}
.demo__section-title {
font-size: 1.375rem;
font-weight: 700;
margin-bottom: 0.375rem;
letter-spacing: -0.01em;
}
.demo__section-desc {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.demo__board-wrapper {
border: 1px solid var(--color-border-primary);
border-radius: 1rem;
overflow: hidden;
min-height: 450px;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.03),
0 8px 24px rgba(0, 0, 0, 0.02);
}
// Custom card template styles
.custom-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-radius: 0.75rem;
padding: 0.75rem;
cursor: grab;
display: flex;
flex-direction: column;
gap: 0.375rem;
transition:
box-shadow 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 250ms cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
}
}
.custom-card__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.custom-card__type {
display: inline-block;
padding: 0.125rem 0.625rem;
border-radius: 999px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
&[data-type="feature"] {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
&[data-type="bug"] {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
&[data-type="task"] {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
&[data-type="improvement"] {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
}
}
.custom-card__points {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-muted);
padding: 0.0625rem 0.5rem;
border-radius: 999px;
background: var(--color-bg-primary);
font-variant-numeric: tabular-nums;
}
.custom-card__title {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
line-height: 1.4;
}
.custom-card__assignees {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.custom-card__assignee {
font-size: 0.6875rem;
color: var(--color-text-secondary);
&::before {
content: '@';
opacity: 0.5;
}
}
.custom-card__labels {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
.custom-card__label {
font-size: 0.625rem;
font-weight: 500;
}

168
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,168 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import {
KbBoardComponent,
KbCardDefDirective,
type KbViewMode,
type KbCardMoveEvent,
type KbCardClickEvent,
type KbCardAddEvent,
type KbColumnToggleEvent,
type KbColumnReorderEvent,
type KbSwimlaneToggleEvent,
type KbViewChangeEvent,
type KbSortChangeEvent,
type KbFilterChangeEvent,
type KbWipExceededEvent,
} from '@sda/kanban-elements-ui';
import {
CARDS, COLUMNS, SWIMLANES, SWIMLANE_CARDS,
WIP_COLUMNS, WIP_CARDS,
type ProjectCard,
} from './mock-data/kanban-data';
import { reorderCards, computeCardOrder } from '@sda/kanban-elements-ui';
type Section = 'basic' | 'pipeline' | 'swimlane' | 'wip' | 'custom' | 'full';
@Component({
selector: 'app-root',
standalone: true,
imports: [KbBoardComponent, KbCardDefDirective],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
readonly darkMode = signal(false);
readonly activeSection = signal<Section>('basic');
readonly lastEvent = signal('');
constructor() {
document.documentElement.setAttribute('data-mode', 'light');
}
// Basic board state
readonly basicCards = signal<ProjectCard[]>([...CARDS]);
readonly columns = COLUMNS;
// Pipeline state
readonly pipelineView = signal<KbViewMode>('pipeline');
// Swimlane state
readonly swimlaneCards = signal<ProjectCard[]>([...SWIMLANE_CARDS]);
readonly swimlanes = SWIMLANES;
// WIP state
readonly wipCards = signal<ProjectCard[]>([...WIP_CARDS]);
readonly wipColumns = WIP_COLUMNS;
// Full-featured state
readonly fullCards = signal<ProjectCard[]>([...CARDS]);
readonly fullViewMode = signal<KbViewMode>('board');
toggleDarkMode(): void {
this.darkMode.update(v => !v);
document.documentElement.setAttribute('data-mode', this.darkMode() ? 'dark' : 'light');
}
setSection(section: Section): void {
this.activeSection.set(section);
}
// --- Basic board handlers ---
onCardMove(event: KbCardMoveEvent): void {
this.lastEvent.set(`Moved "${event.card.title}" from ${event.fromColumnId} to ${event.toColumnId}`);
this.basicCards.update(cards =>
reorderCards(cards, event.card.id, event.toColumnId, event.toIndex) as ProjectCard[]
);
}
onCardClick(event: KbCardClickEvent): void {
this.lastEvent.set(`Clicked: ${event.card.title}`);
}
onCardAdd(event: KbCardAddEvent): void {
this.lastEvent.set(`Added card "${event.title}" to ${event.columnId}`);
this.basicCards.update(cards => {
const newCard: ProjectCard = {
id: `new-${Date.now()}`,
title: event.title,
columnId: event.columnId,
swimlaneId: event.swimlaneId,
order: computeCardOrder(cards, event.columnId),
priority: 'none',
type: 'task',
createdAt: new Date().toISOString(),
};
return [...cards, newCard];
});
}
onColumnToggle(event: KbColumnToggleEvent): void {
this.lastEvent.set(`Column "${event.column.title}" ${event.collapsed ? 'collapsed' : 'expanded'}`);
}
onColumnReorder(event: KbColumnReorderEvent): void {
this.lastEvent.set(`Reordered column "${event.column.title}" from ${event.fromIndex} to ${event.toIndex}`);
}
// --- Swimlane handlers ---
onSwimlaneCardMove(event: KbCardMoveEvent): void {
this.lastEvent.set(`Moved "${event.card.title}" (swimlane: ${event.fromSwimlaneId} -> ${event.toSwimlaneId})`);
this.swimlaneCards.update(cards =>
reorderCards(cards, event.card.id, event.toColumnId, event.toIndex, event.toSwimlaneId) as ProjectCard[]
);
}
onSwimlaneToggle(event: KbSwimlaneToggleEvent): void {
this.lastEvent.set(`Swimlane "${event.swimlane.title}" ${event.collapsed ? 'collapsed' : 'expanded'}`);
}
// --- WIP handlers ---
onWipCardMove(event: KbCardMoveEvent): void {
this.lastEvent.set(`Moved "${event.card.title}" to ${event.toColumnId}`);
this.wipCards.update(cards =>
reorderCards(cards, event.card.id, event.toColumnId, event.toIndex) as ProjectCard[]
);
}
onWipExceeded(event: KbWipExceededEvent): void {
this.lastEvent.set(`WIP EXCEEDED: "${event.column.title}" has ${event.cardCount}/${event.wipLimit} cards`);
}
// --- Full-featured handlers ---
onFullCardMove(event: KbCardMoveEvent): void {
this.lastEvent.set(`Moved "${event.card.title}" to ${event.toColumnId}`);
this.fullCards.update(cards =>
reorderCards(cards, event.card.id, event.toColumnId, event.toIndex) as ProjectCard[]
);
}
onFullCardAdd(event: KbCardAddEvent): void {
this.lastEvent.set(`Added "${event.title}" to ${event.columnId}`);
this.fullCards.update(cards => {
const newCard: ProjectCard = {
id: `new-${Date.now()}`,
title: event.title,
columnId: event.columnId,
order: computeCardOrder(cards, event.columnId),
priority: 'none',
type: 'task',
createdAt: new Date().toISOString(),
};
return [...cards, newCard];
});
}
onViewChange(event: KbViewChangeEvent): void {
this.lastEvent.set(`View changed to ${event.view}`);
this.fullViewMode.set(event.view);
}
onSortChange(event: KbSortChangeEvent): void {
this.lastEvent.set(`Sort: ${event.sort.field} ${event.sort.direction}`);
}
onFilterChange(event: KbFilterChangeEvent): void {
this.lastEvent.set(`Filter updated: ${JSON.stringify(event.filter)}`);
}
}

22
src/app/app.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
import { provideKanbanConfig } from '@sda/kanban-elements-ui';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimations(),
provideKanbanConfig({
defaultView: 'board',
collapsibleColumns: true,
showCardCount: true,
showWipLimits: true,
allowColumnReorder: true,
allowQuickAdd: true,
cardDescriptionLines: 2,
}),
]
};

3
src/app/app.routes.ts Normal file
View File

@@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View File

@@ -0,0 +1,217 @@
import type { KbCard, KbColumn, KbSwimlane, KbLabel, KbAssignee } from '@sda/kanban-elements-ui';
export interface ProjectCard extends KbCard {
type: 'feature' | 'bug' | 'task' | 'improvement';
}
export const LABELS: KbLabel[] = [
{ id: 'l1', text: 'Frontend', color: '#3b82f6' },
{ id: 'l2', text: 'Backend', color: '#8b5cf6' },
{ id: 'l3', text: 'Design', color: '#ec4899' },
{ id: 'l4', text: 'Infrastructure', color: '#f97316' },
{ id: 'l5', text: 'Urgent', color: '#ef4444' },
];
export const ASSIGNEES: KbAssignee[] = [
{ id: 'a1', name: 'Alice Chen', email: 'alice@example.com' },
{ id: 'a2', name: 'Bob Smith', email: 'bob@example.com' },
{ id: 'a3', name: 'Carol Davis', email: 'carol@example.com' },
{ id: 'a4', name: 'David Lee', email: 'david@example.com' },
];
export const COLUMNS: KbColumn[] = [
{ id: 'backlog', title: 'Backlog', order: 0, color: '#6b7280', columnType: 'backlog', allowAdd: true },
{ id: 'todo', title: 'To Do', order: 1, color: '#3b82f6', columnType: 'active', allowAdd: true },
{ id: 'progress', title: 'In Progress', order: 2, color: '#f59e0b', columnType: 'active', wipLimit: 4, allowAdd: false },
{ id: 'review', title: 'Review', order: 3, color: '#8b5cf6', columnType: 'active', wipLimit: 3, allowAdd: false },
{ id: 'done', title: 'Done', order: 4, color: '#22c55e', columnType: 'done', allowAdd: false },
];
export const SWIMLANES: KbSwimlane[] = [
{ id: 'high', title: 'High Priority', order: 0, color: '#ef4444', collapsible: true },
{ id: 'medium', title: 'Medium Priority', order: 1, color: '#f59e0b', collapsible: true },
{ id: 'low', title: 'Low Priority', order: 2, color: '#22c55e', collapsible: true },
];
export const CARDS: ProjectCard[] = [
// Backlog
{
id: 'c1', title: 'Implement user onboarding flow', description: 'Create a step-by-step onboarding wizard for new users including profile setup and preferences.',
columnId: 'backlog', order: 0, priority: 'medium', type: 'feature',
labels: [LABELS[0], LABELS[2]], assignees: [ASSIGNEES[0]], storyPoints: 8,
createdAt: '2025-12-01',
},
{
id: 'c2', title: 'Database migration script for v2', description: 'Write and test migration scripts for the new schema.',
columnId: 'backlog', order: 1, priority: 'high', type: 'task',
labels: [LABELS[1], LABELS[3]], assignees: [ASSIGNEES[1]], storyPoints: 5,
createdAt: '2025-12-05',
},
{
id: 'c3', title: 'Redesign settings page', description: 'Update the settings page UI to match new design system.',
columnId: 'backlog', order: 2, priority: 'low', type: 'improvement',
labels: [LABELS[0], LABELS[2]], storyPoints: 3,
createdAt: '2025-12-10',
},
{
id: 'c4', title: 'Add CSV export to reports',
columnId: 'backlog', order: 3, priority: 'low', type: 'feature',
labels: [LABELS[0]], storyPoints: 2,
createdAt: '2025-12-15',
},
// To Do
{
id: 'c5', title: 'Setup CI/CD pipeline', description: 'Configure automated build, test, and deployment pipeline.',
columnId: 'todo', order: 0, priority: 'high', type: 'task',
labels: [LABELS[3]], assignees: [ASSIGNEES[3]], storyPoints: 5,
dueDate: '2026-02-20', createdAt: '2025-12-20',
},
{
id: 'c6', title: 'Fix login session timeout', description: 'Users are being logged out unexpectedly after 5 minutes.',
columnId: 'todo', order: 1, priority: 'critical', type: 'bug',
labels: [LABELS[1], LABELS[4]], assignees: [ASSIGNEES[1]], storyPoints: 3,
dueDate: '2026-02-15', createdAt: '2025-12-22',
},
{
id: 'c7', title: 'Design notification system mockups', description: 'Create wireframes and high-fidelity mockups for in-app notifications.',
columnId: 'todo', order: 2, priority: 'medium', type: 'task',
labels: [LABELS[2]], assignees: [ASSIGNEES[2]], storyPoints: 3,
dueDate: '2026-02-28', createdAt: '2025-12-25',
},
{
id: 'c8', title: 'Implement dark mode toggle',
columnId: 'todo', order: 3, priority: 'low', type: 'feature',
labels: [LABELS[0]], storyPoints: 2,
createdAt: '2026-01-02',
},
// In Progress
{
id: 'c9', title: 'Build dashboard analytics widgets', description: 'Create reusable chart components for the main dashboard.',
columnId: 'progress', order: 0, priority: 'high', type: 'feature',
labels: [LABELS[0]], assignees: [ASSIGNEES[0]], storyPoints: 8,
dueDate: '2026-02-18', subtasks: { completed: 3, total: 5 },
createdAt: '2026-01-05',
},
{
id: 'c10', title: 'API rate limiting middleware', description: 'Implement rate limiting to prevent API abuse.',
columnId: 'progress', order: 1, priority: 'high', type: 'task',
labels: [LABELS[1]], assignees: [ASSIGNEES[1]], storyPoints: 5,
dueDate: '2026-02-16', createdAt: '2026-01-08',
},
{
id: 'c11', title: 'Fix memory leak in WebSocket handler', description: 'Memory usage increases over time due to unclosed connections.',
columnId: 'progress', order: 2, priority: 'critical', type: 'bug',
labels: [LABELS[1], LABELS[4]], assignees: [ASSIGNEES[3]], storyPoints: 5,
dueDate: '2026-02-14', blocked: true, blockedReason: 'Waiting for DevOps to provide heap dump',
createdAt: '2026-01-10',
},
{
id: 'c12', title: 'User profile page redesign',
columnId: 'progress', order: 3, priority: 'medium', type: 'improvement',
labels: [LABELS[0], LABELS[2]], assignees: [ASSIGNEES[2]], storyPoints: 5,
subtasks: { completed: 2, total: 4 }, createdAt: '2026-01-12',
},
// Review
{
id: 'c13', title: 'Search autocomplete feature', description: 'Add typeahead search suggestions with debouncing.',
columnId: 'review', order: 0, priority: 'medium', type: 'feature',
labels: [LABELS[0], LABELS[1]], assignees: [ASSIGNEES[0], ASSIGNEES[1]], storyPoints: 5,
dueDate: '2026-02-17', createdAt: '2026-01-15',
},
{
id: 'c14', title: 'Update API documentation', description: 'Refresh OpenAPI docs to reflect latest endpoints.',
columnId: 'review', order: 1, priority: 'low', type: 'task',
labels: [LABELS[1]], assignees: [ASSIGNEES[3]], storyPoints: 2,
createdAt: '2026-01-18',
},
{
id: 'c15', title: 'Accessibility audit fixes', description: 'Address WCAG 2.1 AA issues found in the audit.',
columnId: 'review', order: 2, priority: 'high', type: 'improvement',
labels: [LABELS[0]], assignees: [ASSIGNEES[2]], storyPoints: 5,
dueDate: '2026-02-19', createdAt: '2026-01-20',
},
// Done
{
id: 'c16', title: 'Setup project repository',
columnId: 'done', order: 0, priority: 'high', type: 'task',
labels: [LABELS[3]], assignees: [ASSIGNEES[3]], storyPoints: 1,
createdAt: '2025-11-01',
},
{
id: 'c17', title: 'Design system color palette',
columnId: 'done', order: 1, priority: 'medium', type: 'task',
labels: [LABELS[2]], assignees: [ASSIGNEES[2]], storyPoints: 3,
createdAt: '2025-11-10',
},
{
id: 'c18', title: 'Authentication module', description: 'JWT-based auth with refresh tokens.',
columnId: 'done', order: 2, priority: 'critical', type: 'feature',
labels: [LABELS[1]], assignees: [ASSIGNEES[1]], storyPoints: 8,
createdAt: '2025-11-20',
},
{
id: 'c19', title: 'Responsive layout framework',
columnId: 'done', order: 3, priority: 'high', type: 'feature',
labels: [LABELS[0]], assignees: [ASSIGNEES[0]], storyPoints: 5,
createdAt: '2025-12-01',
},
{
id: 'c20', title: 'Fix broken image uploads',
columnId: 'done', order: 4, priority: 'high', type: 'bug',
labels: [LABELS[1]], assignees: [ASSIGNEES[3]], storyPoints: 2,
createdAt: '2025-12-10',
},
];
// Cards with swimlane assignments for the swimlane demo
export const SWIMLANE_CARDS: ProjectCard[] = CARDS.map(card => {
const priority = card.priority ?? 'none';
let swimlaneId: string;
if (priority === 'critical' || priority === 'high') {
swimlaneId = 'high';
} else if (priority === 'medium') {
swimlaneId = 'medium';
} else {
swimlaneId = 'low';
}
return { ...card, swimlaneId };
});
// WIP-focused columns (some deliberately over limit)
export const WIP_COLUMNS: KbColumn[] = [
{ id: 'wip-backlog', title: 'Backlog', order: 0, color: '#6b7280', columnType: 'backlog' },
{ id: 'wip-todo', title: 'To Do', order: 1, color: '#3b82f6', wipLimit: 5 },
{ id: 'wip-progress', title: 'In Progress', order: 2, color: '#f59e0b', wipLimit: 3 },
{ id: 'wip-review', title: 'Review', order: 3, color: '#8b5cf6', wipLimit: 2 },
{ id: 'wip-done', title: 'Done', order: 4, color: '#22c55e', columnType: 'done' },
];
export const WIP_CARDS: ProjectCard[] = [
// Backlog (no limit)
{ id: 'w1', title: 'Research new frameworks', columnId: 'wip-backlog', order: 0, priority: 'low', type: 'task' },
{ id: 'w2', title: 'Plan Q2 roadmap', columnId: 'wip-backlog', order: 1, priority: 'medium', type: 'task' },
// To Do: 4 of 5 (normal)
{ id: 'w3', title: 'Setup monitoring', columnId: 'wip-todo', order: 0, priority: 'high', type: 'task' },
{ id: 'w4', title: 'Write unit tests', columnId: 'wip-todo', order: 1, priority: 'medium', type: 'task' },
{ id: 'w5', title: 'Update dependencies', columnId: 'wip-todo', order: 2, priority: 'low', type: 'task' },
{ id: 'w6', title: 'Code review guidelines', columnId: 'wip-todo', order: 3, priority: 'low', type: 'task' },
// In Progress: 3 of 3 (warning - at limit)
{ id: 'w7', title: 'Build API gateway', columnId: 'wip-progress', order: 0, priority: 'critical', type: 'feature', labels: [LABELS[1]], assignees: [ASSIGNEES[0]] },
{ id: 'w8', title: 'Fix CSS grid layout', columnId: 'wip-progress', order: 1, priority: 'high', type: 'bug', labels: [LABELS[0]], assignees: [ASSIGNEES[2]] },
{ id: 'w9', title: 'Database optimization', columnId: 'wip-progress', order: 2, priority: 'high', type: 'improvement', labels: [LABELS[1]], assignees: [ASSIGNEES[1]] },
// Review: 3 of 2 (exceeded!)
{ id: 'w10', title: 'PR: Auth refactor', columnId: 'wip-review', order: 0, priority: 'high', type: 'task', assignees: [ASSIGNEES[3]] },
{ id: 'w11', title: 'PR: Dashboard widgets', columnId: 'wip-review', order: 1, priority: 'medium', type: 'feature', assignees: [ASSIGNEES[0]] },
{ id: 'w12', title: 'PR: Bug fix batch', columnId: 'wip-review', order: 2, priority: 'medium', type: 'bug', assignees: [ASSIGNEES[1]] },
// Done (no limit)
{ id: 'w13', title: 'Initial setup', columnId: 'wip-done', order: 0, priority: 'high', type: 'task' },
{ id: 'w14', title: 'Landing page', columnId: 'wip-done', order: 1, priority: 'medium', type: 'feature' },
];

14
src/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Kanban Elements Demo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Interactive demo for @sda/kanban-elements-ui library">
<link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>&#x1F4CB;</text></svg>">
</head>
<body data-theme="mist" data-mode="light">
<app-root></app-root>
</body>
</html>

6
src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

120
src/styles.scss Normal file
View File

@@ -0,0 +1,120 @@
// Inter font (with system font stack fallback)
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
// Kanban tokens
@import '@sda/kanban-elements-ui/src/styles/tokens';
// Global reset
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
// Root styles
html, body {
height: 100%;
width: 100%;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--color-bg-primary, #f5f5f7);
color: var(--color-text-primary, #111827);
line-height: 1.5;
}
// Light mode (on :root to override @media prefers-color-scheme)
:root[data-mode="light"] {
--color-bg-primary: #f5f5f7;
--color-bg-secondary: #ffffff;
--color-bg-hover: #f0f0f2;
--color-text-primary: #111827;
--color-text-secondary: #6b7280;
--color-text-muted: #9ca3af;
--color-border-primary: rgba(0, 0, 0, 0.08);
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-700: #1d4ed8;
--color-primary-900: #1e3a5f;
--color-error-500: #ef4444;
--color-border: rgba(0, 0, 0, 0.08);
--color-primary: #3b82f6;
--radius-sm: 0.375rem;
--radius-md: 0.75rem;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 4px rgba(0, 0, 0, 0.03);
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
// Override kanban tokens back to light
--kb-bg: #e8e8ec;
--kb-column-bg: #ffffff;
--kb-column-border: rgba(0, 0, 0, 0.08);
--kb-column-header-color: #111827;
--kb-card-bg: #ffffff;
--kb-card-border: rgba(0, 0, 0, 0.08);
--kb-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.05);
--kb-card-shadow-hover: 0 4px 14px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06);
--kb-card-shadow-dragging: 0 16px 48px rgba(0, 0, 0, 0.12), 0 8px 16px rgba(0, 0, 0, 0.08);
--kb-card-title-color: #111827;
--kb-card-desc-color: #6b7280;
--kb-card-meta-color: #9ca3af;
--kb-swimlane-border: rgba(0, 0, 0, 0.08);
--kb-swimlane-header-color: #111827;
--kb-toolbar-bg: #ffffff;
--kb-toolbar-border: rgba(0, 0, 0, 0.08);
--kb-quick-add-hover-bg: #f0f0f2;
--kb-quick-add-text: #9ca3af;
--kb-pipeline-connector-color: rgba(0, 0, 0, 0.08);
--kb-glass-bg: rgba(255, 255, 255, 0.72);
--kb-glass-border: rgba(255, 255, 255, 0.2);
}
// Dark mode
:root[data-mode="dark"] {
--color-bg-primary: #0c0f17;
--color-bg-secondary: #161b26;
--color-bg-hover: #1e2536;
--color-text-primary: #f9fafb;
--color-text-secondary: #9ca3af;
--color-text-muted: #6b7280;
--color-border-primary: rgba(255, 255, 255, 0.06);
--color-primary-50: #1e293b;
--color-primary-100: #1e3a5f;
--color-primary-500: #60a5fa;
--color-primary-700: #93bbfd;
--color-primary-900: #eff6ff;
--color-error-500: #f87171;
--color-border: rgba(255, 255, 255, 0.06);
--color-primary: #60a5fa;
}
// Scrollbar styling
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
&:hover {
background: var(--color-text-secondary);
}
}
// Focus visible styles
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}

13
tsconfig.app.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

30
tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}