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:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.angular/
|
||||||
|
*.tgz
|
||||||
76
angular.json
Normal file
76
angular.json
Normal 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
13887
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
214
src/app/app.component.html
Normal 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 @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
235
src/app/app.component.scss
Normal 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
168
src/app/app.component.ts
Normal 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
22
src/app/app.config.ts
Normal 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
3
src/app/app.routes.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export const routes: Routes = [];
|
||||||
217
src/app/mock-data/kanban-data.ts
Normal file
217
src/app/mock-data/kanban-data.ts
Normal 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
14
src/index.html
Normal 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>📋</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body data-theme="mist" data-mode="light">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
src/main.ts
Normal file
6
src/main.ts
Normal 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
120
src/styles.scss
Normal 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
13
tsconfig.app.json
Normal 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
30
tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user