ng-hub-ui-board
Version:
An Angular-based Kanban board component with Trello-like drag-and-drop, customizable columns, and straightforward event handling.
1,050 lines • 90.8 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, input, computed, signal, contentChild, TemplateRef, viewChild, output, Component, NgModule, Pipe } from '@angular/core';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { moveItemInArray, transferArrayItem, createNativeDragImage } from 'ng-hub-ui-utils';
export { moveItemInArray, transferArrayItem } from 'ng-hub-ui-utils';
/**
* Directive that allows customization of column footer templates within board columns.
*
* This directive enables developers to define custom templates for column footers,
* perfect for displaying summary information, quick actions, statistics,
* or any column-specific controls at the bottom of each column.
*
* @publicApi
*
* @example
* ```html
* <ng-template columnFooterTpt let-column="column">
* <div class="custom-footer">
* <div class="column-summary">
* <span>Total: {{ column.cards.length }}</span>
* <span>Priority Items: {{ getPriorityItems(column) }}</span>
* </div>
* <button (click)="quickAddCard(column)">Quick Add</button>
* </div>
* </ng-template>
* ```
*/
class BoardColumnFooterDirective {
templateRef;
/**
* Creates a new BoardColumnFooterDirective instance.
*
* @param templateRef - The template reference that contains the custom column footer layout
*/
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardColumnFooterDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: BoardColumnFooterDirective, isStandalone: true, selector: "[columnFooterTpt]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardColumnFooterDirective, decorators: [{
type: Directive,
args: [{
selector: '[columnFooterTpt]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Directive that allows customization of column header templates within board columns.
*
* This directive provides the ability to define custom templates for rendering column headers,
* giving developers full control over the appearance and functionality of column headers
* including titles, descriptions, actions, and metadata display.
*
* @publicApi
*
* @example
* ```html
* <ng-template columnHeaderTpt let-column="column">
* <div class="custom-header">
* <h2>{{ column.title }}</h2>
* <span class="card-count">{{ column.cards.length }} items</span>
* <button (click)="addCard(column)">Add Card</button>
* </div>
* </ng-template>
* ```
*/
class BoardColumnHeaderDirective {
templateRef;
/**
* Creates a new BoardColumnHeaderDirective instance.
*
* @param templateRef - The template reference that contains the custom column header layout
*/
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardColumnHeaderDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: BoardColumnHeaderDirective, isStandalone: true, selector: "[columnHeaderTpt]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardColumnHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: '[columnHeaderTpt]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Directive that allows customization of the drag preview (drag image) for cards.
*
* This directive defines a custom template for the visual element that follows
* the cursor when dragging a card. The template receives the dragged card and
* the source column as context variables.
*
* @publicApi
*
* @example
* ```html
* <hub-board [board]="board">
* <ng-template cardDragPreview let-card="card" let-column="column">
* <div class="custom-drag-preview">
* <i class="bi bi-grip-vertical"></i>
* <span>{{ card.title }}</span>
* </div>
* </ng-template>
* </hub-board>
* ```
*/
class CardDragPreviewDirective {
templateRef;
/**
* Creates a new CardDragPreviewDirective instance.
*
* @param templateRef - The template reference that contains the custom drag preview layout
*/
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardDragPreviewDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: CardDragPreviewDirective, isStandalone: true, selector: "[cardDragPreview]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardDragPreviewDirective, decorators: [{
type: Directive,
args: [{
selector: '[cardDragPreview]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Structural directive used to define a custom placeholder template for cards during drag operations.
*
* When a card is being dragged, the placeholder shows where the card will be dropped.
* By default, an empty space with a dashed border is shown. Use this directive to customize
* the placeholder appearance.
*
* @usageNotes
*
* ### Basic usage
*
* ```html
* <hub-board [board]="board">
* <ng-template cardPlaceholder let-card="card" let-column="column">
* <div class="my-custom-placeholder">
* <span>Drop "{{ card?.title }}" here</span>
* </div>
* </ng-template>
* </hub-board>
* ```
*
* ### Context variables
*
* The template context provides:
* - `card`: The card being dragged (may be undefined)
* - `column`: The target column where the card will be dropped
*
* @publicApi
*/
class CardPlaceholderDirective {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardPlaceholderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: CardPlaceholderDirective, isStandalone: true, selector: "[cardPlaceholder]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardPlaceholderDirective, decorators: [{
type: Directive,
args: [{
selector: '[cardPlaceholder]',
standalone: true
}]
}] });
/**
* Directive that allows customization of card templates within board columns.
*
* This directive is used to define custom templates for rendering board cards.
* It provides access to the template reference that can be used by the board component
* to render cards with custom layouts and styling.
*
* @publicApi
*
* @example
* ```html
* <ng-template cardTpt let-card="item" let-column="column">
* <div class="custom-card">
* <h3>{{ card.title }}</h3>
* <p>{{ card.description }}</p>
* </div>
* </ng-template>
* ```
*/
class CardTemplateDirective {
templateRef;
/**
* Creates a new CardTemplateDirective instance.
*
* @param templateRef - The template reference that contains the custom card layout
*/
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: CardTemplateDirective, isStandalone: true, selector: "[cardTpt]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CardTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[cardTpt]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Directive that allows customization of the drag preview (drag image) for columns.
*
* This directive defines a custom template for the visual element that follows
* the cursor when dragging a column. The template receives the dragged column
* as a context variable.
*
* @publicApi
*
* @example
* ```html
* <hub-board [board]="board">
* <ng-template columnDragPreview let-column="column">
* <div class="custom-column-drag-preview">
* <i class="bi bi-kanban"></i>
* <span>{{ column.title }}</span>
* <span class="badge">{{ column.cards.length }} cards</span>
* </div>
* </ng-template>
* </hub-board>
* ```
*/
class ColumnDragPreviewDirective {
templateRef;
/**
* Creates a new ColumnDragPreviewDirective instance.
*
* @param templateRef - The template reference that contains the custom drag preview layout
*/
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ColumnDragPreviewDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: ColumnDragPreviewDirective, isStandalone: true, selector: "[columnDragPreview]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ColumnDragPreviewDirective, decorators: [{
type: Directive,
args: [{
selector: '[columnDragPreview]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Structural directive used to define a custom placeholder template for columns during drag operations.
*
* When a column is being dragged, the placeholder shows where the column will be dropped.
* By default, an empty space with a dashed border is shown. Use this directive to customize
* the placeholder appearance.
*
* @usageNotes
*
* ### Basic usage
*
* ```html
* <hub-board [board]="board">
* <ng-template columnPlaceholder let-column="column">
* <div class="my-custom-placeholder">
* <span>Drop "{{ column?.title }}" here</span>
* </div>
* </ng-template>
* </hub-board>
* ```
*
* ### Context variables
*
* The template context provides:
* - `column`: The column being dragged (may be undefined)
*
* @publicApi
*/
class ColumnPlaceholderDirective {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ColumnPlaceholderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: ColumnPlaceholderDirective, isStandalone: true, selector: "[columnPlaceholder]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ColumnPlaceholderDirective, decorators: [{
type: Directive,
args: [{
selector: '[columnPlaceholder]',
standalone: true
}]
}] });
/**
* Array move/transfer helpers used when reordering cards and columns.
*
* These come from the shared `ng-hub-ui-utils` drag-and-drop core — a dependency-free,
* behaviour-identical implementation shared by every ng-hub-ui library — and are re-exported
* here so the board's public API is unchanged.
*
* @publicApi
*/
/**
* Standalone Kanban-style board component that provides column-based drag-and-drop,
* custom templates and infinite-scroll detection.
*
* @publicApi
*/
class HubBoardComponent {
/**
* Reactive input containing the full board definition (columns and cards).
*/
board = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "board" }] : /* istanbul ignore next */ []));
/**
* Semantic accent applied to the drag/drop placeholder. The built-in values
* (`primary` / `success` / `danger` / `warning` / `info`) render with the
* design-system tints; any other string is also accepted — the board reads
* `--hub-sys-color-<variant>` from the host application, so a custom accent
* palette interconnects with no changes to this library. Defaults to `primary`.
*/
variant = input(undefined, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
/**
* Inline accent fed to the board styles: `var(--hub-sys-color-<variant>)` for
* the active variant, or `null` to keep the `primary` default. Keeps the
* variant set open to any accent token the host application defines.
*/
groupAccent = computed(() => {
const variant = this.variant();
return variant ? `var(--hub-sys-color-${variant})` : null;
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "groupAccent" }] : /* istanbul ignore next */ []));
/**
* Pixel threshold used when determining whether a column has reached scroll end.
* Allows for fractional scroll values across different browsers.
*/
scrollDetectionPadding = 1;
/**
* Internal signal to track column updates and force re-renders.
*/
_columnsVersion = signal(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_columnsVersion" }] : /* istanbul ignore next */ []));
/**
* Derived list of board columns exposed as a signal to the template.
* Depends on both the board input and internal version counter to ensure
* re-renders after in-place array mutations.
*/
columns = computed(() => {
// Subscribe to version changes to force re-computation after mutations
this._columnsVersion();
return this.board()?.columns ?? [];
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
/**
* When true, column reordering via drag-and-drop is disabled.
*/
columnSortingDisabled = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "columnSortingDisabled" }] : /* istanbul ignore next */ []));
/**
* Controls how dragged elements behave visually during drag operations.
* - 'ghost': Element becomes semi-transparent (50% opacity) but remains visible
* - 'hide': Element is hidden but still occupies its space
* - 'collapse': Element is completely hidden and its space is collapsed (default)
*/
dragBehavior = input('collapse', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "dragBehavior" }] : /* istanbul ignore next */ []));
/**
* Custom card template supplied via the `cardTpt` structural directive.
*/
cardTpt = contentChild(CardTemplateDirective, { ...(ngDevMode ? { debugName: "cardTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom column header template supplied via the `columnHeaderTpt` structural directive.
*/
columnHeaderTpt = contentChild(BoardColumnHeaderDirective, { ...(ngDevMode ? { debugName: "columnHeaderTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom column footer template supplied via the `columnFooterTpt` structural directive.
*/
columnFooterTpt = contentChild(BoardColumnFooterDirective, { ...(ngDevMode ? { debugName: "columnFooterTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom card placeholder template supplied via the `cardPlaceholder` structural directive.
* Used to customize the appearance of the drop zone when dragging cards.
*/
cardPlaceholderTpt = contentChild(CardPlaceholderDirective, { ...(ngDevMode ? { debugName: "cardPlaceholderTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom column placeholder template supplied via the `columnPlaceholder` structural directive.
* Used to customize the appearance of the drop zone when dragging columns.
*/
columnPlaceholderTpt = contentChild(ColumnPlaceholderDirective, { ...(ngDevMode ? { debugName: "columnPlaceholderTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom card drag preview template supplied via the `cardDragPreview` structural directive.
* Used to customize the visual element that follows the cursor when dragging cards.
* The template receives `card` (the dragged card) and `column` (the source column) as context.
*/
cardDragPreviewTpt = contentChild(CardDragPreviewDirective, { ...(ngDevMode ? { debugName: "cardDragPreviewTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Custom column drag preview template supplied via the `columnDragPreview` structural directive.
* Used to customize the visual element that follows the cursor when dragging columns.
* The template receives `column` (the dragged column) as context.
*/
columnDragPreviewTpt = contentChild(ColumnDragPreviewDirective, { ...(ngDevMode ? { debugName: "columnDragPreviewTpt" } : /* istanbul ignore next */ {}), read: (TemplateRef) });
/**
* Reference to the hidden container where drag preview elements are rendered.
*/
dragPreviewContainer = viewChild('dragPreviewContainer', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "dragPreviewContainer" }] : /* istanbul ignore next */ []));
/**
* Disposer for the currently active custom drag preview, or `null`.
*/
dragPreviewDestroy = null;
/**
* Emits each time a card is clicked within the board.
*/
onCardClick = output();
/**
* Emits when a card has been repositioned, either within the same column or into another column.
*/
onCardMoved = output();
/**
* Emits when columns are reordered through drag-and-drop.
*/
onColumnMoved = output();
/**
* Emits when a column body is scrolled to its end, enabling infinite-scroll behaviour.
*/
reachedEnd = output();
/**
* Internal drag state tracking.
*/
dragState = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "dragState" }] : /* istanbul ignore next */ []));
/**
* Signal for tracking the currently hovered column index during card drag.
*/
hoveredColumnIndex = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hoveredColumnIndex" }] : /* istanbul ignore next */ []));
/**
* Signal for tracking the drop indicator position within a column.
*/
dropIndicatorIndex = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "dropIndicatorIndex" }] : /* istanbul ignore next */ []));
/**
* Signal for tracking the column drop indicator position.
*/
columnDropIndicatorIndex = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "columnDropIndicatorIndex" }] : /* istanbul ignore next */ []));
/**
* Default predicate that allows any card to be dropped into any column.
*
* @returns Always `true`, indicating that drop operations are permitted.
*/
defaultEnterPredicateFn = () => true;
/**
* Returns the card currently being dragged, if any.
*
* @returns The dragged card or null if no card is being dragged.
*/
get draggedCard() {
const state = this.dragState();
return state?.type === 'card' ? state.item : null;
}
/**
* Returns the column currently being dragged, if any.
*
* @returns The dragged column or null if no column is being dragged.
*/
get draggedColumn() {
const state = this.dragState();
return state?.type === 'column' ? state.item : null;
}
/**
* Emits the clicked card through {@link onCardClick}.
*
* @param item - The card that triggered the click event.
*/
cardClick(item) {
this.onCardClick.emit(item);
}
/**
* Track function for columns to ensure proper re-rendering.
* Uses column id if available, otherwise falls back to index.
*
* @param index - The index of the column.
* @param column - The column object.
* @returns A unique identifier for the column.
*/
trackColumnById(index, column) {
return column.id ?? index;
}
/**
* Checks if the given column is currently being dragged.
*
* @param column - The column to check.
* @returns Whether the column is being dragged.
*/
isDraggingColumn(column) {
const state = this.dragState();
return state?.type === 'column' && state.item === column;
}
/**
* Checks if the given card is currently being dragged.
*
* @param card - The card to check.
* @returns Whether the card is being dragged.
*/
isDraggingCard(card) {
const state = this.dragState();
return state?.type === 'card' && state.item === card;
}
/**
* Handles the drag start event for columns.
*
* @param event - The native drag event.
* @param column - The column being dragged.
* @param columnIndex - The index of the column in the board.
*/
onColumnDragStart(event, column, columnIndex) {
if (this.columnSortingDisabled() || column.disabled) {
event.preventDefault();
return;
}
const element = event.currentTarget;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', `column:${columnIndex}`);
// Use custom drag preview if template is provided
const previewTemplate = this.columnDragPreviewTpt();
if (previewTemplate) {
const previewElement = this.createDragPreview(previewTemplate, { column });
if (previewElement) {
event.dataTransfer.setDragImage(previewElement, 0, 0);
}
}
}
// Set drag state after a micro-delay to allow the browser to capture the drag image
// before we hide the element
requestAnimationFrame(() => {
this.dragState.set({
type: 'column',
sourceColumnIndex: columnIndex,
item: column,
element
});
});
}
/**
* Handles the drag end event for columns.
*
* @param _event - The native drag event (unused).
*/
onColumnDragEnd(_event) {
this.destroyDragPreview();
this.columnDropIndicatorIndex.set(null);
this.dragState.set(null);
}
/**
* Handles the drag over event for the board container.
* Calculates the drop position based on mouse position relative to columns.
*
* @param event - The native drag event.
*/
onBoardDragOver(event) {
const state = this.dragState();
if (!state || state.type !== 'column') {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
// Find which column we're over based on mouse position
const boardElement = event.currentTarget;
const columnElements = boardElement.querySelectorAll('.hub-board__column-container');
const mouseX = event.clientX;
let dropIndex = this.columns().length; // Default to end
for (let i = 0; i < columnElements.length; i++) {
const columnElement = columnElements[i];
const rect = columnElement.getBoundingClientRect();
const columnMiddle = rect.left + rect.width / 2;
if (mouseX < columnMiddle) {
dropIndex = i;
break;
}
else if (mouseX < rect.right) {
dropIndex = i + 1;
break;
}
}
this.columnDropIndicatorIndex.set(dropIndex);
}
/**
* Handles the drop event for the board container (column reordering).
*
* @param event - The native drag event.
*/
onBoardDrop(event) {
event.preventDefault();
const state = this.dragState();
if (!state || state.type !== 'column') {
return;
}
const dropIndicator = this.columnDropIndicatorIndex();
if (dropIndicator === null) {
// No valid drop position - reset state
this.columnDropIndicatorIndex.set(null);
this.dragState.set(null);
return;
}
const columns = this.columns();
const previousIndex = state.sourceColumnIndex;
// Calculate the actual target index
// If dropping after the source, we need to subtract 1 because the source will be removed first
let currentIndex = dropIndicator;
if (dropIndicator > previousIndex) {
currentIndex = dropIndicator - 1;
}
if (previousIndex !== currentIndex) {
moveItemInArray(columns, previousIndex, currentIndex);
// Force re-render by incrementing the version counter
this._columnsVersion.update((v) => v + 1);
const dropEvent = {
previousIndex,
currentIndex,
container: { data: columns },
previousContainer: { data: columns },
item: { data: state.item },
isPointerOverContainer: true
};
this.onColumnMoved.emit(dropEvent);
}
this.columnDropIndicatorIndex.set(null);
this.dragState.set(null);
}
/**
* Handles the drag start event for cards.
*
* @param event - The native drag event.
* @param card - The card being dragged.
* @param columnIndex - The index of the column containing the card.
* @param cardIndex - The index of the card within the column.
*/
onCardDragStart(event, card, columnIndex, cardIndex) {
if (card.disabled) {
event.preventDefault();
return;
}
event.stopPropagation();
const element = event.currentTarget;
const sourceColumn = this.columns()[columnIndex];
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', `card:${columnIndex}:${cardIndex}`);
// Use custom drag preview if template is provided
const previewTemplate = this.cardDragPreviewTpt();
if (previewTemplate) {
const previewElement = this.createDragPreview(previewTemplate, { card, column: sourceColumn });
if (previewElement) {
event.dataTransfer.setDragImage(previewElement, 0, 0);
}
}
}
// Set drag state after a micro-delay to allow the browser to capture the drag image
// before we hide the element
requestAnimationFrame(() => {
this.dragState.set({
type: 'card',
sourceColumnIndex: columnIndex,
sourceCardIndex: cardIndex,
item: card,
element
});
});
}
/**
* Handles the drag end event for cards.
*
* @param _event - The native drag event (unused).
*/
onCardDragEnd(_event) {
this.destroyDragPreview();
this.hoveredColumnIndex.set(null);
this.dropIndicatorIndex.set(null);
this.dragState.set(null);
}
/**
* Handles the drag over event for column bodies (card drop zones).
*
* @param event - The native drag event.
* @param column - The column being dragged over.
* @param columnIndex - The index of the column.
*/
onCardDragOver(event, column, columnIndex) {
const state = this.dragState();
if (!state || state.type !== 'card') {
return;
}
// Check if the column accepts this card via predicate
const predicate = column.predicate ?? this.defaultEnterPredicateFn;
const dragItem = { data: state.item, element: state.element };
if (!predicate(dragItem) || column.cardSortingDisabled) {
return;
}
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
this.hoveredColumnIndex.set(columnIndex);
// Calculate drop position based on mouse Y position
const containerElement = event.currentTarget;
const cards = containerElement.querySelectorAll('.hub-board__card:not(.hub-board__card--dragging)');
const mouseY = event.clientY;
let dropIndex = column.cards.length;
for (let i = 0; i < cards.length; i++) {
const cardElement = cards[i];
const rect = cardElement.getBoundingClientRect();
const cardMiddle = rect.top + rect.height / 2;
if (mouseY < cardMiddle) {
// Adjust index to account for the dragged card if in same column
if (state.sourceColumnIndex === columnIndex && state.sourceCardIndex !== undefined) {
dropIndex = i <= state.sourceCardIndex ? i : i;
}
else {
dropIndex = i;
}
break;
}
}
this.dropIndicatorIndex.set(dropIndex);
}
/**
* Handles the drag leave event for column bodies.
*
* @param event - The native drag event.
* @param columnIndex - The index of the column being left.
*/
onCardDragLeave(event, columnIndex) {
const relatedTarget = event.relatedTarget;
const currentTarget = event.currentTarget;
// Only clear if we're actually leaving the container (not entering a child)
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
if (this.hoveredColumnIndex() === columnIndex) {
this.hoveredColumnIndex.set(null);
this.dropIndicatorIndex.set(null);
}
}
}
/**
* Handles the drop event for cards.
*
* @param event - The native drag event.
* @param column - The target column.
* @param columnIndex - The index of the target column.
*/
onCardDrop(event, column, columnIndex) {
event.preventDefault();
event.stopPropagation();
const state = this.dragState();
if (!state || state.type !== 'card' || state.sourceCardIndex === undefined) {
return;
}
const columns = this.columns();
const sourceColumn = columns[state.sourceColumnIndex];
const targetColumn = column;
const previousIndex = state.sourceCardIndex;
let currentIndex = this.dropIndicatorIndex() ?? targetColumn.cards.length;
// Adjust for same-column moves
if (state.sourceColumnIndex === columnIndex && previousIndex < currentIndex) {
currentIndex--;
}
const isSameContainer = state.sourceColumnIndex === columnIndex;
if (isSameContainer) {
if (previousIndex !== currentIndex) {
moveItemInArray(targetColumn.cards, previousIndex, currentIndex);
}
}
else {
transferArrayItem(sourceColumn.cards, targetColumn.cards, previousIndex, currentIndex);
}
// Force re-render by incrementing the version counter
this._columnsVersion.update((v) => v + 1);
const dropEvent = {
previousIndex,
currentIndex,
container: { data: targetColumn },
previousContainer: { data: sourceColumn },
item: { data: state.item },
isPointerOverContainer: true
};
this.onCardMoved.emit(dropEvent);
this.hoveredColumnIndex.set(null);
this.dropIndicatorIndex.set(null);
this.dragState.set(null);
}
/**
* Determines if a column should show the drop indicator.
*
* @param columnIndex - The index position to check for the drop indicator.
* @returns Whether the drop indicator should be shown at this position.
*/
shouldShowColumnDropIndicator(columnIndex) {
const state = this.dragState();
if (state?.type !== 'column') {
return false;
}
return this.columnDropIndicatorIndex() === columnIndex;
}
/**
* Determines if a card drop indicator should be shown at a specific position.
*
* @param columnIndex - The index of the column.
* @param cardIndex - The index where the indicator would appear.
* @returns Whether the drop indicator should be shown.
*/
shouldShowCardDropIndicator(columnIndex, cardIndex) {
const state = this.dragState();
if (state?.type !== 'card') {
return false;
}
return this.hoveredColumnIndex() === columnIndex && this.dropIndicatorIndex() === cardIndex;
}
/**
* Emits {@link reachedEnd} once a column body is scrolled to its bottom.
*
* @param index - Index of the scrolled column within the board.
* @param event - Browser scroll event originating from the column body element.
*/
onScroll(index, event) {
const el = event.target;
if (!el) {
return;
}
const scrolledToBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - this.scrollDetectionPadding;
if (!scrolledToBottom) {
return;
}
const column = this.board()?.columns?.[index];
if (!column) {
return;
}
this.reachedEnd.emit({
index,
data: column
});
}
/**
* Creates a custom drag preview element from a template and renders it in a hidden container.
* The element must be in the DOM for setDragImage to work properly.
*
* @param template - The template to render as the drag preview.
* @param context - The context to pass to the template.
* @returns The native HTML element to use as the drag image, or null if creation failed.
*/
createDragPreview(template, context) {
// Destroy any existing preview first.
this.destroyDragPreview();
const container = this.dragPreviewContainer();
if (!container) {
return null;
}
// Render the template into the hidden container using the shared drag-image helper.
const image = createNativeDragImage(template, context, container.nativeElement);
if (!image) {
return null;
}
this.dragPreviewDestroy = image.destroy;
return image.node;
}
/**
* Destroys the current drag preview view and cleans up related resources. The disposer
* returned by `createNativeDragImage` removes both the embedded view and the rendered
* nodes, so no manual container clearing is required.
*/
destroyDragPreview() {
this.dragPreviewDestroy?.();
this.dragPreviewDestroy = null;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: HubBoardComponent, isStandalone: true, selector: "hub-board, hub-ui-board", inputs: { board: { classPropertyName: "board", publicName: "board", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, columnSortingDisabled: { classPropertyName: "columnSortingDisabled", publicName: "columnSortingDisabled", isSignal: true, isRequired: false, transformFunction: null }, dragBehavior: { classPropertyName: "dragBehavior", publicName: "dragBehavior", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onCardClick: "onCardClick", onCardMoved: "onCardMoved", onColumnMoved: "onColumnMoved", reachedEnd: "reachedEnd" }, host: { properties: { "attr.data-variant": "variant() ?? null", "style.--hub-board-accent": "groupAccent()" }, classAttribute: "hub-board" }, queries: [{ propertyName: "cardTpt", first: true, predicate: CardTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "columnHeaderTpt", first: true, predicate: BoardColumnHeaderDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "columnFooterTpt", first: true, predicate: BoardColumnFooterDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cardPlaceholderTpt", first: true, predicate: CardPlaceholderDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "columnPlaceholderTpt", first: true, predicate: ColumnPlaceholderDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cardDragPreviewTpt", first: true, predicate: CardDragPreviewDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "columnDragPreviewTpt", first: true, predicate: ColumnDragPreviewDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "dragPreviewContainer", first: true, predicate: ["dragPreviewContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (columns().length) {\n<div class=\"hub-board__drop-list\" (dragover)=\"onBoardDragOver($event)\" (drop)=\"onBoardDrop($event)\">\n\t<div class=\"hub-board__columns\">\n\t\t@for (column of columns(); let index = $index; track trackColumnById($index, column)) {\n\t\t<!-- Column placeholder (before column) -->\n\t\t@if (shouldShowColumnDropIndicator(index)) {\n\t\t<div class=\"hub-board__column-placeholder\">\n\t\t\t@if (columnPlaceholderTpt()) {\n\t\t\t<ng-container\n\t\t\t\t[ngTemplateOutlet]=\"columnPlaceholderTpt()!\"\n\t\t\t\t[ngTemplateOutletContext]=\"{ column: draggedColumn }\"\n\t\t\t></ng-container>\n\t\t\t}\n\t\t</div>\n\t\t}\n\t\t<div\n\t\t\tclass=\"hub-board__column-container\"\n\t\t\t[class.hub-board__column-container--disabled]=\"column.disabled\"\n\t\t\t[class.hub-board__column-container--dragging-ghost]=\"isDraggingColumn(column) && dragBehavior() === 'ghost'\"\n\t\t\t[class.hub-board__column-container--dragging-hide]=\"isDraggingColumn(column) && dragBehavior() === 'hide'\"\n\t\t\t[class.hub-board__column-container--dragging-collapse]=\"isDraggingColumn(column) && dragBehavior() === 'collapse'\"\n\t\t\t[attr.draggable]=\"!columnSortingDisabled() && !column.disabled\"\n\t\t\t[attr.data-column-index]=\"index\"\n\t\t\t(dragstart)=\"onColumnDragStart($event, column, index)\"\n\t\t\t(dragend)=\"onColumnDragEnd($event)\"\n\t\t>\n\t\t\t<div class=\"hub-board__column\" [ngClass]=\"column.classlist\" [style]=\"column.style\">\n\t\t\t\t<div class=\"hub-board__column-header\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"columnHeaderTpt() || defaultColumnHeaderTpt\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ column: column }\"\n\t\t\t\t\t>\n\t\t\t\t\t</ng-container>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"hub-board__column-body\"\n\t\t\t\t\t[class.hub-board__column-body--drag-over]=\"hoveredColumnIndex() === index\"\n\t\t\t\t\t(scroll)=\"onScroll(index, $event)\"\n\t\t\t\t\t(dragover)=\"onCardDragOver($event, column, index)\"\n\t\t\t\t\t(dragleave)=\"onCardDragLeave($event, index)\"\n\t\t\t\t\t(drop)=\"onCardDrop($event, column, index)\"\n\t\t\t\t>\n\t\t\t\t\t@for (card of column.cards; let cardIndex = $index; track card) {\n\t\t\t\t\t<!-- Card placeholder (before card) -->\n\t\t\t\t\t@if (shouldShowCardDropIndicator(index, cardIndex)) {\n\t\t\t\t\t<div class=\"hub-board__card-placeholder\">\n\t\t\t\t\t\t@if (cardPlaceholderTpt()) {\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardPlaceholderTpt()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ card: draggedCard, column: column }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"hub-board__card\"\n\t\t\t\t\t\t[class.hub-board__card--disabled]=\"card.disabled\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-ghost]=\"isDraggingCard(card) && dragBehavior() === 'ghost'\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-hide]=\"isDraggingCard(card) && dragBehavior() === 'hide'\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-collapse]=\"isDraggingCard(card) && dragBehavior() === 'collapse'\"\n\t\t\t\t\t\t[attr.draggable]=\"!card.disabled && !column.cardSortingDisabled\"\n\t\t\t\t\t\t(dragstart)=\"onCardDragStart($event, card, index, cardIndex)\"\n\t\t\t\t\t\t(dragend)=\"onCardDragEnd($event)\"\n\t\t\t\t\t\t(click)=\"cardClick(card)\"\n\t\t\t\t\t\t(mousedown)=\"card.disabled ? $event.stopPropagation() : null\"\n\t\t\t\t\t\t[ngClass]=\"card.classlist\"\n\t\t\t\t\t\t[style]=\"card.style\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"hub-board__card-body\">\n\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardTpt() || defaultCardTpt\"\n\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{\n\t\t\t\t\t\t\t\t\titem: card,\n\t\t\t\t\t\t\t\t\tcolumn\n\t\t\t\t\t\t\t\t}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</ng-container>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\t<!-- Card placeholder at the end -->\n\t\t\t\t\t@if (shouldShowCardDropIndicator(index, column.cards.length)) {\n\t\t\t\t\t<div class=\"hub-board__card-placeholder\">\n\t\t\t\t\t\t@if (cardPlaceholderTpt()) {\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardPlaceholderTpt()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ card: draggedCard, column: column }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t@if (columnFooterTpt()) {\n\t\t\t\t<div class=\"hub-board__column-footer\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"columnFooterTpt() ?? null\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"{\n\t\t\t\t\t\t\tcolumn: column\n\t\t\t\t\t\t}\"\n\t\t\t\t\t>\n\t\t\t\t\t</ng-container>\n\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\t\t</div>\n\t\t}\n\t\t<!-- Column placeholder at the end -->\n\t\t@if (shouldShowColumnDropIndicator(columns().length)) {\n\t\t<div class=\"hub-board__column-placeholder\">\n\t\t\t@if (columnPlaceholderTpt()) {\n\t\t\t<ng-container\n\t\t\t\t[ngTemplateOutlet]=\"columnPlaceholderTpt()!\"\n\t\t\t\t[ngTemplateOutletContext]=\"{ column: draggedColumn }\"\n\t\t\t></ng-container>\n\t\t\t}\n\t\t</div>\n\t\t}\n\t</div>\n</div>\n}\n\n<ng-template #defaultCardTpt let-item=\"item\">\n\t<h6 class=\"hub-board__card-title\">{{ item.title }}</h6>\n\t<p class=\"hub-board__card-subtitle\">{{ item.description }}</p>\n</ng-template>\n\n<ng-template #defaultColumnHeaderTpt let-column=\"column\">\n\t<div class=\"d-flex flex-column\">\n\t\t<h5 class=\"hub-board__column-header-title\">\n\t\t\t{{ column.title }}\n\t\t</h5>\n\t\t<h6 class=\"hub-board__column-header-subtitle\">\n\t\t\t{{ column.description }}\n\t\t</h6>\n\t</div>\n</ng-template>\n\n<!-- Hidden container for rendering drag preview elements -->\n<div #dragPreviewContainer class=\"hub-board__drag-preview-container\" aria-hidden=\"true\"></div>\n", styles: [":root,:host{--hub-board-container-color: var(--hub-sys-text-primary, #212529);--hub-board-container-bg: var(--hub-sys-surface-page, #fff);--hub-board-border-width: var(--hub-ref-border-width, 1px);--hub-board-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-board-border-radius: var(--hub-ref-radius-md, .375rem);--hub-board-columns-gap: var(--hub-ref-space-3, 1rem);--hub-board-column-width: 256px;--hub-board-column-min-height: 200px;--hub-board-column-body-min-height: 128px;--hub-board-column-body-gap: var(--hub-ref-space-3, 1rem);--hub-board-column-spacer-y: .75rem;--hub-board-column-spacer-x: var(--hub-ref-space-3, 1rem);--hub-board-column-border-width: var(--hub-board-border-width);--hub-board-column-border-color: var(--hub-board-border-color);--hub-board-column-border-radius: var(--hub-board-border-radius);--hub-board-column-box-shadow: none;--hub-board-column-inner-border-radius: var(--hub-board-border-radius);--hub-board-column-cap-padding-y: var(--hub-ref-space-2, .5rem);--hub-board-column-cap-padding-x: var(--hub-ref-space-3, 1rem);--hub-board-column-cap-bg: var(--hub-sys-surface-elevated, #f8f9fa);--hub-board-column-cap-color: inherit;--hub-board-column-header-padding-y: var(--hub-board-column-cap-padding-y);--hub-board-column-header-padding-x: var(--hub-board-column-cap-padding-x);--hub-board-column-header-bg: var(--hub-board-column-cap-bg);--hub-board-column-header-color: var(--hub-board-column-cap-color);--hub-board-column-footer-padding-y: var(--hub-board-column-cap-padding-y);--hub-board-column-footer-padding-x: var(--hub-board-column-cap-padding-x);--hub-board-column-footer-bg: var(--hub-board-column-cap-bg);--hub-board-column-footer-color: var(--hub-board-column-cap-color);--hub-board-column-body-padding-y: var(--hub-board-column-spacer-y);--hub-board-column-body-padding-x: var(--hub-board-column-spacer-x);--hub-board-column-header-title-color: inherit;--hub-board-column-header-title-spacer-y: var(--hub-ref-space-2, .5rem);--hub-board-column-header-subtitle-color: var(--hub-sys-text-muted, #6c757d);--hub-board-column-height: 100%;--hub-board-column-color: var(--hub-board-container-color);--hub-board-column-bg: var(--hub-board-container-bg);--hub-board-card-spacer-y: .75rem;--hub-board-card-spacer-x: var(--hub-ref-space-3, 1rem);--hub-board-card-title-spacer-y: var(--hub-ref-space-2, .5rem);--hub-board-card-title-color: inherit;--hub-board-card-subtitle-color: var(--hub-sys-text-muted, #6c757d);--hub-board-card-border-width: var(--hub-board-border-width);--hub-board-card-border-color: var(--hub-board-border-color);--hub-board-card-border-radius: var(--hub-board-border-radius);--hub-board-card-box-shadow: none;--hub-board-card-inner-border-radius: calc( var(--hub-board-card-border-radius) - var(--hub-board-card-border-width) );--hub-board-card-cap-padding-y: var(--hub-ref-space-2, .5rem);--hub-board-card-cap-padding-x: var(--hub-ref-space-3, 1rem);--hub-board-card-cap-bg: var(--hub-sys-surface-elevated, #f8f9fa);--hub-board-card-cap-color: inherit;--hub-board-card-padding-y: var(--hub-board-card-spacer-y);--hub-board-card-padding-x: var(--hub-board-card-spacer-x);--hub-board-card-header-padding-y: var(--hub-board-card-cap-padding-y);--hub-board-card-header-padding-x: var(--hub-board-card-cap-padding-x);--hub-board-card-header-bg: var(--hub-board-card-cap-bg);--hub-board-card-header-color: var(--hub-board-card-cap-color);--hub-board-card-footer-padding-y: var(--hub-board-card-cap-padding-y);--hub-board-card-footer-padding-x: var(--hub-board-card-cap-padding-x);--hub-board-card-footer-bg: var(--hub-board-card-cap-bg);--hub-board-card-footer-color: var(--hub-board-card-cap-color);--hub-board-card-height: auto;--hub-board-card-color: var(--hub-board-container-color);--hub-board-card-bg: var(--hub-board-container-bg);--hub-board-drag-transition: transform .25s cubic-bezier(0, 0, .2, 1);--hub-board-accent: var(--hub-sys-color-primary, #0d6efd);--hub-board-accent-emphasis: color-mix(in oklch, var(--hub-board-accent) 80%, var(--hub-sys-color-ink, #212529));--hub-board-accent-subtle: color-mix(in oklch, var(--hub-board-accent) 12%, var(--hub-sys-surface-page, #fff));--hub-board-accent-on: oklch(from var(--hub-board-accent) clamp(0, (.62 - l) * 1000, 1) 0 h);--hub-board-placeholder-border-color: var(--hub-board-accent);--hub-board-placeholder-border-width: 2px;--hub-board-placeholder-border-style: dashed;--hub-board-placeholder-bg: var(--hub-board-accent-subtle);--hub-board-placeholder-min-height: 60px}:host{display:block;overflow:auto;width:100%;height:100%}.hub-board__drop-list{height:100%}.hub-board__columns{display:flex;flex-wrap:nowrap;width:100%;height:100%;gap:var(--hub-board-columns-gap, var(--hub-ref-space-3, 1rem))}.hub-board__columns .hub-board__column-container{height:100%}.hub-board__column-container{cursor:grab}.hub-board__column-container--dragging-ghost{opacity:.4;pointer-events:none}.hub-board__column-container--dragging-hide{visibility:hidden;opacity:0;pointer-events:none}.hub-board__column-container--dragging-collapse{display:none}.hub-board__column-container--disabled{cursor:default}.hub-board__column-container[draggable=true]{cursor:grab}.hub-board__column-container[draggable=true]:active{cursor:grabbing}.hub-board__column-container[draggable=false]{cursor:default}.hub-board__column-placeholder{width:var(--hub-board-column-width, 256px);min-width:var(--hub-board-column-width, 256px);height:100%;min-height:var(--hub-board-column-min-height, 200px);border:var(--hub-board-placeholder-border-width, 2px) var(--hub-board-placeholder-border-style, dashed) var(--hub-board-placeholder-border-color, var(--hub-sys-color-primary, #0d6efd));border-radius:var(--hub-board-column-border-radius, var(--hub-ref-radius-md, .375rem));background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)));flex-shrink:0;display:flex;align-items:center;justify-content:center;animation:hub-board-placeholder-fade-in .15s ease-out}.hub-board__column{position:relative;display:flex;flex-direction:column;min-width:0;height:var(--hub-board-column-height, 100%);max-height:100%;width:var(--hub-board-column-width, 256px);color:var(--hub-board-container-color, var(--hub-sys-text-primary, #212529));word-wrap:break-word;background-color:var(--hub-board-column-bg, var(--hub-sys-surface-page, #fff));background-clip:border-box;border:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6));border-radius:var(--hub-board-column-border-radius, var(--hub-ref-radius-md, .375rem));box-shadow:var(--hub-board-column-box-shadow, none)}.hub-board__column-body{flex:1 1 auto;padding:var(--hub-board-column-body-padding-y) var(--hub-board-column-body-padding-x);color:var(--hub-board-column-color, var(--hub-board-container-color));overflow:auto;display:flex;flex-direction:column;gap:var(--hub-board-column-body-gap, var(--hub-ref-space-3, 1rem));min-height:var(--hub-board-column-body-min-height, 128px)}.hub-board__column-body--drag-over{background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)))}.hub-board__column-header{padding:var(--hub-board-column-header-padding-y) var(--hub-board-column-header-padding-x);margin:0;color:var(--hub-board-column-header-color);background-color:var(--hub-board-column-header-bg);border-bottom:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__column-header-title{margin-bottom:var(--hub-board-column-header-title-spacer-y, var(--hub-ref-space-2, .5rem));color:var(--hub-board-column-header-title-color, inherit)}.hub-board__column-header-subtitle{margin-top:calc(-.5 * var(--hub-board-column-header-title-spacer-y, var(--hub-ref-space-2, .5rem)));margin-bottom:0;color:var(--hub-board-column-header-subtitle-color, var(--hub-sys-text-muted, #6c757d))}.hub-board__column-header:first-child{border-top-left-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-top-right-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-bottom-right-radius:0;border-bottom-left-radius:0}.hub-board__column-footer{padding:var(--hub-board-column-footer-padding-y) var(--hub-board-column-footer-padding-x);color:var(--hub-board-column-footer-color);background-color:var(--hub-board-column-footer-bg);border-top:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__column-footer:last-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-bottom-left-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)))}.hub-board__card{position:relative;display:flex;flex-direction:column;min-width:0;height:var(--hub-board-card-height, auto);color:var(--hub-board-container-color, var(--hub-sys-text-primary, #212529));word-wrap:break-word;background-color:var(--hub-board-card-bg, var(--hub-sys-surface-page, #fff));background-clip:border-box;border:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6));border-radius:var(--hub-board-card-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));box-shadow:var(--hub-board-card-box-shadow, none);cursor:grab;-webkit-user-select:none;user-select:none;transition:var(--hub-board-drag-transition, transform .25s cubic-bezier(0, 0, .2, 1))}.hub-board__card--dragging-ghost{opacity:.4;pointer-events:none}.hub-board__card--dragging-hide{visibility:hidden;opacity:0;pointer-events:none}.hub-board__card--dragging-collapse{display:none}.hub-board__card--disabled{cursor:default;opacity:.6}.hub-board__card[draggable=true]{cursor:grab}.hub-board__card[draggable=true]:active{cursor:grabbing}.hub-board__card[draggable=false]{cursor:default}.hub-board__card>hr{margin-right:0;margin-left:0}.hub-board__card>.list-group{border-top:inherit;border-bottom:inherit}.hub-board__card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-top-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card>.list-group:last-child{border-bottom-width:0;border-bottom-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-bottom-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card>.hub-board__card-header+.list-group,.hub-board__card>.list-group+.hub-board__card-footer{border-top:0}.hub-board__card-body{flex:1 1 auto;padding:var(--hub-board-card-padding-y) var(--hub-board-card-padding-x);color:var(--hub-board-card-color, var(--hub-board-container-color))}.hub-board__card-title{margin-bottom:var(--hub-board-card-title-spacer-y, var(--hub-ref-space-2, .5rem));color:var(--hub-board-card-title-color, inherit)}.hub-board__card-subtitle{margin-top:calc(-.5 * var(--hub-board-card-title-spacer-y, var(--hub-ref-space-2, .5rem)));margin-bottom:0;color:var(--hub-board-card-subtitle-color, var(--hub-sys-text-muted, #6c757d))}.hub-board__card-header{padding:var(--hub-board-card-header-padding-y) var(--hub-board-card-header-padding-x);color:var(--hub-board-card-header-color);background-color:var(--hub-board-card-header-bg);border-bottom:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__card-header:first-child{border-top-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-top-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card-footer{padding:var(--hub-board-card-footer-padding-y) var(--hub-board-card-footer-padding-x);color:var(--hub-board-card-footer-color);background-color:var(--hub-board-card-footer-bg);border-top:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__card-footer:last-child{border-bottom-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-bottom-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card-placeholder{min-height:var(--hub-board-placeholder-min-height, 60px);border:var(--hub-board-placeholder-border-width, 2px) var(--hub-board-placeholder-border-style, dashed) var(--hub-board-placeholder-border-color, var(--hub-sys-color-primary, #0d6efd));border-radius:var(--hub-board-card-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)));display:flex;align-items:center;justify-content:center;animation:hub-board-placeholder-fade-in .15s ease-out}.hub-board__drag-preview-container{position:fixed;top:-9999px;left:-9999px;pointer-events:none;z-index:-1}@keyframes hub-board-placeholder-fade-in{0%{opacity:0;transform:scaleY(.8)}to{opacity:1;transform:scaleY(1)}}:host([data-variant=primary]),.hub-board[data-variant=primary]{--hub-board-accent: var(--hub-sys-color-primary)}:host([data-variant=secondary]),.hub-board[data-variant=secondary]{--hub-board-accent: var(--hub-sys-color-secondary)}:host([data-variant=success]),.hub-board[data-variant=success]{--hub-board-accent: var(--hub-sys-color-success)}:host([data-variant=danger]),.hub-board[data-variant=danger]{--hub-board-accent: var(--hub-sys-color-danger)}:host([data-variant=warning]),.hub-board[data-variant=warning]{--hub-board-accent: var(--hub-sys-color-warning)}:host([data-variant=info]),.hub-board[data-variant=info]{--hub-board-accent: var(--hub-sys-color-info)}:host([data-variant=neutral]),.hub-board[data-variant=neutral]{--hub-board-accent: var(--hub-sys-color-neutral)}:host([data-variant=light]),.hub-board[data-variant=light]{--hub-board-accent: var(--hub-sys-color-light)}:host([data-variant=dark]),.hub-board[data-variant=dark]{--hub-board-accent: var(--hub-sys-color-dark)}:host([data-variant]),.hub-board[data-variant]{--hub-board-accent-emphasis: color-mix(in oklch, var(--hub-board-accent) 80%, var(--hub-sys-color-ink, #212529));--hub-board-accent-subtle: color-mix(in oklch, var(--hub-board-accent) 12%, var(--hub-sys-surface-page, #fff));--hub-board-accent-on: oklch(from var(--hub-board-accent) clamp(0, (.62 - l) * 1000, 1) 0 h)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubBoardComponent, decorators: [{
type: Component,
args: [{ selector: 'hub-board, hub-ui-board', imports: [NgClass, NgTemplateOutlet], host: {
class: 'hub-board',
'[attr.data-variant]': 'variant() ?? null',
'[style.--hub-board-accent]': 'groupAccent()'
}, template: "@if (columns().length) {\n<div class=\"hub-board__drop-list\" (dragover)=\"onBoardDragOver($event)\" (drop)=\"onBoardDrop($event)\">\n\t<div class=\"hub-board__columns\">\n\t\t@for (column of columns(); let index = $index; track trackColumnById($index, column)) {\n\t\t<!-- Column placeholder (before column) -->\n\t\t@if (shouldShowColumnDropIndicator(index)) {\n\t\t<div class=\"hub-board__column-placeholder\">\n\t\t\t@if (columnPlaceholderTpt()) {\n\t\t\t<ng-container\n\t\t\t\t[ngTemplateOutlet]=\"columnPlaceholderTpt()!\"\n\t\t\t\t[ngTemplateOutletContext]=\"{ column: draggedColumn }\"\n\t\t\t></ng-container>\n\t\t\t}\n\t\t</div>\n\t\t}\n\t\t<div\n\t\t\tclass=\"hub-board__column-container\"\n\t\t\t[class.hub-board__column-container--disabled]=\"column.disabled\"\n\t\t\t[class.hub-board__column-container--dragging-ghost]=\"isDraggingColumn(column) && dragBehavior() === 'ghost'\"\n\t\t\t[class.hub-board__column-container--dragging-hide]=\"isDraggingColumn(column) && dragBehavior() === 'hide'\"\n\t\t\t[class.hub-board__column-container--dragging-collapse]=\"isDraggingColumn(column) && dragBehavior() === 'collapse'\"\n\t\t\t[attr.draggable]=\"!columnSortingDisabled() && !column.disabled\"\n\t\t\t[attr.data-column-index]=\"index\"\n\t\t\t(dragstart)=\"onColumnDragStart($event, column, index)\"\n\t\t\t(dragend)=\"onColumnDragEnd($event)\"\n\t\t>\n\t\t\t<div class=\"hub-board__column\" [ngClass]=\"column.classlist\" [style]=\"column.style\">\n\t\t\t\t<div class=\"hub-board__column-header\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"columnHeaderTpt() || defaultColumnHeaderTpt\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ column: column }\"\n\t\t\t\t\t>\n\t\t\t\t\t</ng-container>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"hub-board__column-body\"\n\t\t\t\t\t[class.hub-board__column-body--drag-over]=\"hoveredColumnIndex() === index\"\n\t\t\t\t\t(scroll)=\"onScroll(index, $event)\"\n\t\t\t\t\t(dragover)=\"onCardDragOver($event, column, index)\"\n\t\t\t\t\t(dragleave)=\"onCardDragLeave($event, index)\"\n\t\t\t\t\t(drop)=\"onCardDrop($event, column, index)\"\n\t\t\t\t>\n\t\t\t\t\t@for (card of column.cards; let cardIndex = $index; track card) {\n\t\t\t\t\t<!-- Card placeholder (before card) -->\n\t\t\t\t\t@if (shouldShowCardDropIndicator(index, cardIndex)) {\n\t\t\t\t\t<div class=\"hub-board__card-placeholder\">\n\t\t\t\t\t\t@if (cardPlaceholderTpt()) {\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardPlaceholderTpt()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ card: draggedCard, column: column }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"hub-board__card\"\n\t\t\t\t\t\t[class.hub-board__card--disabled]=\"card.disabled\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-ghost]=\"isDraggingCard(card) && dragBehavior() === 'ghost'\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-hide]=\"isDraggingCard(card) && dragBehavior() === 'hide'\"\n\t\t\t\t\t\t[class.hub-board__card--dragging-collapse]=\"isDraggingCard(card) && dragBehavior() === 'collapse'\"\n\t\t\t\t\t\t[attr.draggable]=\"!card.disabled && !column.cardSortingDisabled\"\n\t\t\t\t\t\t(dragstart)=\"onCardDragStart($event, card, index, cardIndex)\"\n\t\t\t\t\t\t(dragend)=\"onCardDragEnd($event)\"\n\t\t\t\t\t\t(click)=\"cardClick(card)\"\n\t\t\t\t\t\t(mousedown)=\"card.disabled ? $event.stopPropagation() : null\"\n\t\t\t\t\t\t[ngClass]=\"card.classlist\"\n\t\t\t\t\t\t[style]=\"card.style\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"hub-board__card-body\">\n\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardTpt() || defaultCardTpt\"\n\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{\n\t\t\t\t\t\t\t\t\titem: card,\n\t\t\t\t\t\t\t\t\tcolumn\n\t\t\t\t\t\t\t\t}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</ng-container>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\t<!-- Card placeholder at the end -->\n\t\t\t\t\t@if (shouldShowCardDropIndicator(index, column.cards.length)) {\n\t\t\t\t\t<div class=\"hub-board__card-placeholder\">\n\t\t\t\t\t\t@if (cardPlaceholderTpt()) {\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"cardPlaceholderTpt()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ card: draggedCard, column: column }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t@if (columnFooterTpt()) {\n\t\t\t\t<div class=\"hub-board__column-footer\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"columnFooterTpt() ?? null\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"{\n\t\t\t\t\t\t\tcolumn: column\n\t\t\t\t\t\t}\"\n\t\t\t\t\t>\n\t\t\t\t\t</ng-container>\n\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\t\t</div>\n\t\t}\n\t\t<!-- Column placeholder at the end -->\n\t\t@if (shouldShowColumnDropIndicator(columns().length)) {\n\t\t<div class=\"hub-board__column-placeholder\">\n\t\t\t@if (columnPlaceholderTpt()) {\n\t\t\t<ng-container\n\t\t\t\t[ngTemplateOutlet]=\"columnPlaceholderTpt()!\"\n\t\t\t\t[ngTemplateOutletContext]=\"{ column: draggedColumn }\"\n\t\t\t></ng-container>\n\t\t\t}\n\t\t</div>\n\t\t}\n\t</div>\n</div>\n}\n\n<ng-template #defaultCardTpt let-item=\"item\">\n\t<h6 class=\"hub-board__card-title\">{{ item.title }}</h6>\n\t<p class=\"hub-board__card-subtitle\">{{ item.description }}</p>\n</ng-template>\n\n<ng-template #defaultColumnHeaderTpt let-column=\"column\">\n\t<div class=\"d-flex flex-column\">\n\t\t<h5 class=\"hub-board__column-header-title\">\n\t\t\t{{ column.title }}\n\t\t</h5>\n\t\t<h6 class=\"hub-board__column-header-subtitle\">\n\t\t\t{{ column.description }}\n\t\t</h6>\n\t</div>\n</ng-template>\n\n<!-- Hidden container for rendering drag preview elements -->\n<div #dragPreviewContainer class=\"hub-board__drag-preview-container\" aria-hidden=\"true\"></div>\n", styles: [":root,:host{--hub-board-container-color: var(--hub-sys-text-primary, #212529);--hub-board-container-bg: var(--hub-sys-surface-page, #fff);--hub-board-border-width: var(--hub-ref-border-width, 1px);--hub-board-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-board-border-radius: var(--hub-ref-radius-md, .375rem);--hub-board-columns-gap: var(--hub-ref-space-3, 1rem);--hub-board-column-width: 256px;--hub-board-column-min-height: 200px;--hub-board-column-body-min-height: 128px;--hub-board-column-body-gap: var(--hub-ref-space-3, 1rem);--hub-board-column-spacer-y: .75rem;--hub-board-column-spacer-x: var(--hub-ref-space-3, 1rem);--hub-board-column-border-width: var(--hub-board-border-width);--hub-board-column-border-color: var(--hub-board-border-color);--hub-board-column-border-radius: var(--hub-board-border-radius);--hub-board-column-box-shadow: none;--hub-board-column-inner-border-radius: var(--hub-board-border-radius);--hub-board-column-cap-padding-y: var(--hub-ref-space-2, .5rem);--hub-board-column-cap-padding-x: var(--hub-ref-space-3, 1rem);--hub-board-column-cap-bg: var(--hub-sys-surface-elevated, #f8f9fa);--hub-board-column-cap-color: inherit;--hub-board-column-header-padding-y: var(--hub-board-column-cap-padding-y);--hub-board-column-header-padding-x: var(--hub-board-column-cap-padding-x);--hub-board-column-header-bg: var(--hub-board-column-cap-bg);--hub-board-column-header-color: var(--hub-board-column-cap-color);--hub-board-column-footer-padding-y: var(--hub-board-column-cap-padding-y);--hub-board-column-footer-padding-x: var(--hub-board-column-cap-padding-x);--hub-board-column-footer-bg: var(--hub-board-column-cap-bg);--hub-board-column-footer-color: var(--hub-board-column-cap-color);--hub-board-column-body-padding-y: var(--hub-board-column-spacer-y);--hub-board-column-body-padding-x: var(--hub-board-column-spacer-x);--hub-board-column-header-title-color: inherit;--hub-board-column-header-title-spacer-y: var(--hub-ref-space-2, .5rem);--hub-board-column-header-subtitle-color: var(--hub-sys-text-muted, #6c757d);--hub-board-column-height: 100%;--hub-board-column-color: var(--hub-board-container-color);--hub-board-column-bg: var(--hub-board-container-bg);--hub-board-card-spacer-y: .75rem;--hub-board-card-spacer-x: var(--hub-ref-space-3, 1rem);--hub-board-card-title-spacer-y: var(--hub-ref-space-2, .5rem);--hub-board-card-title-color: inherit;--hub-board-card-subtitle-color: var(--hub-sys-text-muted, #6c757d);--hub-board-card-border-width: var(--hub-board-border-width);--hub-board-card-border-color: var(--hub-board-border-color);--hub-board-card-border-radius: var(--hub-board-border-radius);--hub-board-card-box-shadow: none;--hub-board-card-inner-border-radius: calc( var(--hub-board-card-border-radius) - var(--hub-board-card-border-width) );--hub-board-card-cap-padding-y: var(--hub-ref-space-2, .5rem);--hub-board-card-cap-padding-x: var(--hub-ref-space-3, 1rem);--hub-board-card-cap-bg: var(--hub-sys-surface-elevated, #f8f9fa);--hub-board-card-cap-color: inherit;--hub-board-card-padding-y: var(--hub-board-card-spacer-y);--hub-board-card-padding-x: var(--hub-board-card-spacer-x);--hub-board-card-header-padding-y: var(--hub-board-card-cap-padding-y);--hub-board-card-header-padding-x: var(--hub-board-card-cap-padding-x);--hub-board-card-header-bg: var(--hub-board-card-cap-bg);--hub-board-card-header-color: var(--hub-board-card-cap-color);--hub-board-card-footer-padding-y: var(--hub-board-card-cap-padding-y);--hub-board-card-footer-padding-x: var(--hub-board-card-cap-padding-x);--hub-board-card-footer-bg: var(--hub-board-card-cap-bg);--hub-board-card-footer-color: var(--hub-board-card-cap-color);--hub-board-card-height: auto;--hub-board-card-color: var(--hub-board-container-color);--hub-board-card-bg: var(--hub-board-container-bg);--hub-board-drag-transition: transform .25s cubic-bezier(0, 0, .2, 1);--hub-board-accent: var(--hub-sys-color-primary, #0d6efd);--hub-board-accent-emphasis: color-mix(in oklch, var(--hub-board-accent) 80%, var(--hub-sys-color-ink, #212529));--hub-board-accent-subtle: color-mix(in oklch, var(--hub-board-accent) 12%, var(--hub-sys-surface-page, #fff));--hub-board-accent-on: oklch(from var(--hub-board-accent) clamp(0, (.62 - l) * 1000, 1) 0 h);--hub-board-placeholder-border-color: var(--hub-board-accent);--hub-board-placeholder-border-width: 2px;--hub-board-placeholder-border-style: dashed;--hub-board-placeholder-bg: var(--hub-board-accent-subtle);--hub-board-placeholder-min-height: 60px}:host{display:block;overflow:auto;width:100%;height:100%}.hub-board__drop-list{height:100%}.hub-board__columns{display:flex;flex-wrap:nowrap;width:100%;height:100%;gap:var(--hub-board-columns-gap, var(--hub-ref-space-3, 1rem))}.hub-board__columns .hub-board__column-container{height:100%}.hub-board__column-container{cursor:grab}.hub-board__column-container--dragging-ghost{opacity:.4;pointer-events:none}.hub-board__column-container--dragging-hide{visibility:hidden;opacity:0;pointer-events:none}.hub-board__column-container--dragging-collapse{display:none}.hub-board__column-container--disabled{cursor:default}.hub-board__column-container[draggable=true]{cursor:grab}.hub-board__column-container[draggable=true]:active{cursor:grabbing}.hub-board__column-container[draggable=false]{cursor:default}.hub-board__column-placeholder{width:var(--hub-board-column-width, 256px);min-width:var(--hub-board-column-width, 256px);height:100%;min-height:var(--hub-board-column-min-height, 200px);border:var(--hub-board-placeholder-border-width, 2px) var(--hub-board-placeholder-border-style, dashed) var(--hub-board-placeholder-border-color, var(--hub-sys-color-primary, #0d6efd));border-radius:var(--hub-board-column-border-radius, var(--hub-ref-radius-md, .375rem));background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)));flex-shrink:0;display:flex;align-items:center;justify-content:center;animation:hub-board-placeholder-fade-in .15s ease-out}.hub-board__column{position:relative;display:flex;flex-direction:column;min-width:0;height:var(--hub-board-column-height, 100%);max-height:100%;width:var(--hub-board-column-width, 256px);color:var(--hub-board-container-color, var(--hub-sys-text-primary, #212529));word-wrap:break-word;background-color:var(--hub-board-column-bg, var(--hub-sys-surface-page, #fff));background-clip:border-box;border:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6));border-radius:var(--hub-board-column-border-radius, var(--hub-ref-radius-md, .375rem));box-shadow:var(--hub-board-column-box-shadow, none)}.hub-board__column-body{flex:1 1 auto;padding:var(--hub-board-column-body-padding-y) var(--hub-board-column-body-padding-x);color:var(--hub-board-column-color, var(--hub-board-container-color));overflow:auto;display:flex;flex-direction:column;gap:var(--hub-board-column-body-gap, var(--hub-ref-space-3, 1rem));min-height:var(--hub-board-column-body-min-height, 128px)}.hub-board__column-body--drag-over{background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)))}.hub-board__column-header{padding:var(--hub-board-column-header-padding-y) var(--hub-board-column-header-padding-x);margin:0;color:var(--hub-board-column-header-color);background-color:var(--hub-board-column-header-bg);border-bottom:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__column-header-title{margin-bottom:var(--hub-board-column-header-title-spacer-y, var(--hub-ref-space-2, .5rem));color:var(--hub-board-column-header-title-color, inherit)}.hub-board__column-header-subtitle{margin-top:calc(-.5 * var(--hub-board-column-header-title-spacer-y, var(--hub-ref-space-2, .5rem)));margin-bottom:0;color:var(--hub-board-column-header-subtitle-color, var(--hub-sys-text-muted, #6c757d))}.hub-board__column-header:first-child{border-top-left-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-top-right-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-bottom-right-radius:0;border-bottom-left-radius:0}.hub-board__column-footer{padding:var(--hub-board-column-footer-padding-y) var(--hub-board-column-footer-padding-x);color:var(--hub-board-column-footer-color);background-color:var(--hub-board-column-footer-bg);border-top:var(--hub-board-column-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-column-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__column-footer:last-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));border-bottom-left-radius:var(--hub-board-column-inner-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)))}.hub-board__card{position:relative;display:flex;flex-direction:column;min-width:0;height:var(--hub-board-card-height, auto);color:var(--hub-board-container-color, var(--hub-sys-text-primary, #212529));word-wrap:break-word;background-color:var(--hub-board-card-bg, var(--hub-sys-surface-page, #fff));background-clip:border-box;border:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6));border-radius:var(--hub-board-card-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));box-shadow:var(--hub-board-card-box-shadow, none);cursor:grab;-webkit-user-select:none;user-select:none;transition:var(--hub-board-drag-transition, transform .25s cubic-bezier(0, 0, .2, 1))}.hub-board__card--dragging-ghost{opacity:.4;pointer-events:none}.hub-board__card--dragging-hide{visibility:hidden;opacity:0;pointer-events:none}.hub-board__card--dragging-collapse{display:none}.hub-board__card--disabled{cursor:default;opacity:.6}.hub-board__card[draggable=true]{cursor:grab}.hub-board__card[draggable=true]:active{cursor:grabbing}.hub-board__card[draggable=false]{cursor:default}.hub-board__card>hr{margin-right:0;margin-left:0}.hub-board__card>.list-group{border-top:inherit;border-bottom:inherit}.hub-board__card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-top-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card>.list-group:last-child{border-bottom-width:0;border-bottom-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-bottom-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card>.hub-board__card-header+.list-group,.hub-board__card>.list-group+.hub-board__card-footer{border-top:0}.hub-board__card-body{flex:1 1 auto;padding:var(--hub-board-card-padding-y) var(--hub-board-card-padding-x);color:var(--hub-board-card-color, var(--hub-board-container-color))}.hub-board__card-title{margin-bottom:var(--hub-board-card-title-spacer-y, var(--hub-ref-space-2, .5rem));color:var(--hub-board-card-title-color, inherit)}.hub-board__card-subtitle{margin-top:calc(-.5 * var(--hub-board-card-title-spacer-y, var(--hub-ref-space-2, .5rem)));margin-bottom:0;color:var(--hub-board-card-subtitle-color, var(--hub-sys-text-muted, #6c757d))}.hub-board__card-header{padding:var(--hub-board-card-header-padding-y) var(--hub-board-card-header-padding-x);color:var(--hub-board-card-header-color);background-color:var(--hub-board-card-header-bg);border-bottom:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__card-header:first-child{border-top-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-top-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card-footer{padding:var(--hub-board-card-footer-padding-y) var(--hub-board-card-footer-padding-x);color:var(--hub-board-card-footer-color);background-color:var(--hub-board-card-footer-bg);border-top:var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px)) solid var(--hub-board-card-border-color, var(--hub-sys-border-color-default, #dee2e6))}.hub-board__card-footer:last-child{border-bottom-left-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))));border-bottom-right-radius:var(--hub-board-card-inner-border-radius, calc(var(--hub-board-card-border-radius, var(--hub-ref-radius-md, .375rem)) - var(--hub-board-card-border-width, var(--hub-ref-border-width, 1px))))}.hub-board__card-placeholder{min-height:var(--hub-board-placeholder-min-height, 60px);border:var(--hub-board-placeholder-border-width, 2px) var(--hub-board-placeholder-border-style, dashed) var(--hub-board-placeholder-border-color, var(--hub-sys-color-primary, #0d6efd));border-radius:var(--hub-board-card-border-radius, var(--hub-board-border-radius, var(--hub-ref-radius-md, .375rem)));background-color:var(--hub-board-placeholder-bg, var(--hub-sys-color-primary-subtle, rgba(13, 110, 253, .05)));display:flex;align-items:center;justify-content:center;animation:hub-board-placeholder-fade-in .15s ease-out}.hub-board__drag-preview-container{position:fixed;top:-9999px;left:-9999px;pointer-events:none;z-index:-1}@keyframes hub-board-placeholder-fade-in{0%{opacity:0;transform:scaleY(.8)}to{opacity:1;transform:scaleY(1)}}:host([data-variant=primary]),.hub-board[data-variant=primary]{--hub-board-accent: var(--hub-sys-color-primary)}:host([data-variant=secondary]),.hub-board[data-variant=secondary]{--hub-board-accent: var(--hub-sys-color-secondary)}:host([data-variant=success]),.hub-board[data-variant=success]{--hub-board-accent: var(--hub-sys-color-success)}:host([data-variant=danger]),.hub-board[data-variant=danger]{--hub-board-accent: var(--hub-sys-color-danger)}:host([data-variant=warning]),.hub-board[data-variant=warning]{--hub-board-accent: var(--hub-sys-color-warning)}:host([data-variant=info]),.hub-board[data-variant=info]{--hub-board-accent: var(--hub-sys-color-info)}:host([data-variant=neutral]),.hub-board[data-variant=neutral]{--hub-board-accent: var(--hub-sys-color-neutral)}:host([data-variant=light]),.hub-board[data-variant=light]{--hub-board-accent: var(--hub-sys-color-light)}:host([data-variant=dark]),.hub-board[data-variant=dark]{--hub-board-accent: var(--hub-sys-color-dark)}:host([data-variant]),.hub-board[data-variant]{--hub-board-accent-emphasis: color-mix(in oklch, var(--hub-board-accent) 80%, var(--hub-sys-color-ink, #212529));--hub-board-accent-subtle: color-mix(in oklch, var(--hub-board-accent) 12%, var(--hub-sys-surface-page, #fff));--hub-board-accent-on: oklch(from var(--hub-board-accent) clamp(0, (.62 - l) * 1000, 1) 0 h)}\n"] }]
}], propDecorators: { board: [{ type: i0.Input, args: [{ isSignal: true, alias: "board", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], columnSortingDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnSortingDisabled", required: false }] }], dragBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragBehavior", required: false }] }], cardTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CardTemplateDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], columnHeaderTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => BoardColumnHeaderDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], columnFooterTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => BoardColumnFooterDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], cardPlaceholderTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CardPlaceholderDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], columnPlaceholderTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ColumnPlaceholderDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], cardDragPreviewTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CardDragPreviewDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], columnDragPreviewTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ColumnDragPreviewDirective), { ...{
read: (TemplateRef)
}, isSignal: true }] }], dragPreviewContainer: [{ type: i0.ViewChild, args: ['dragPreviewContainer', { isSignal: true }] }], onCardClick: [{ type: i0.Output, args: ["onCardClick"] }], onCardMoved: [{ type: i0.Output, args: ["onCardMoved"] }], onColumnMoved: [{ type: i0.Output, args: ["onColumnMoved"] }], reachedEnd: [{ type: i0.Output, args: ["reachedEnd"] }] } });
/**
* Angular module that provides board functionality with drag-and-drop support.
*
* This module includes all the necessary components and directives for creating
* Kanban-style boards with customizable columns, cards, and templates.
*
* @deprecated Use standalone components instead. Import individual components and directives directly.
* @publicApi
*
* @example
* ```typescript
* // Legacy module approach (not recommended)
* import { BoardModule } from 'ng-hub-ui-board';
*
* @NgModule({
* imports: [BoardModule]
* })
* export class AppModule {}
*
* // Recommended standalone approach
* import { HubBoardComponent, CardTemplateDirective } from 'ng-hub-ui-board';
*
* @Component({
* standalone: true,
* imports: [HubBoardComponent, CardTemplateDirective]
* })
* export class MyComponent {}
* ```
*/
class BoardModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: BoardModule, imports: [HubBoardComponent,
CardTemplateDirective,
BoardColumnHeaderDirective,
BoardColumnFooterDirective,
CardPlaceholderDirective,
ColumnPlaceholderDirective], exports: [HubBoardComponent,
CardTemplateDirective,
BoardColumnHeaderDirective,
BoardColumnFooterDirective,
CardPlaceholderDirective,
ColumnPlaceholderDirective] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BoardModule, decorators: [{
type: NgModule,
args: [{
declarations: [],
imports: [
HubBoardComponent,
CardTemplateDirective,
BoardColumnHeaderDirective,
BoardColumnFooterDirective,
CardPlaceholderDirective,
ColumnPlaceholderDirective
],
exports: [
HubBoardComponent,
CardTemplateDirective,
BoardColumnHeaderDirective,
BoardColumnFooterDirective,
CardPlaceholderDirective,
ColumnPlaceholderDirective
]
}]
}] });
/**
* Converts a hexadecimal color string into its inverted counterpart, offering both
* high-contrast black/white and full-spectrum inversion modes.
*
* @publicApi
*/
class InvertColorPipe {
/**
* Inverts a HEX color value.
*
* @param hex - Color expressed as a 3- or 6-digit HEX string with or without a hash prefix.
* @param bw - When `true`, returns either black or white based on perceived brightness to maximise contrast.
* @returns The inverted color represented as a 6-digit HEX string (always prefixed with `#`).
* @throws Error if the provided value cannot be parsed as a valid HEX color.
*/
transform(hex, bw) {
if (!hex) {
return '#000000';
}
let normalizedHex = hex;
if (normalizedHex.indexOf('#') === 0) {
normalizedHex = normalizedHex.slice(1);
}
if (normalizedHex.length === 3) {
// Convert shorthand notation (e.g., #abc) to full length (#aabbcc)
normalizedHex =
normalizedHex[0] +
normalizedHex[0] +
normalizedHex[1] +
normalizedHex[1] +
normalizedHex[2] +
normalizedHex[2];
}
if (normalizedHex.length !== 6) {
throw new Error('Invalid HEX color.');
}
const r = parseInt(normalizedHex.slice(0, 2), 16);
const g = parseInt(normalizedHex.slice(2, 4), 16);
const b = parseInt(normalizedHex.slice(4, 6), 16);
if (bw) {
// http://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
}
const invertedR = (255 - r).toString(16).padStart(2, '0');
const invertedG = (255 - g).toString(16).padStart(2, '0');
const invertedB = (255 - b).toString(16).padStart(2, '0');
return `#${invertedR}${invertedG}${invertedB}`;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: InvertColorPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: InvertColorPipe, isStandalone: true, name: "invertColor" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: InvertColorPipe, decorators: [{
type: Pipe,
args: [{
name: 'invertColor',
standalone: true
}]
}] });
// The array move/transfer helpers used when reordering cards and columns now live in the
// shared `ng-hub-ui-utils` drag-and-drop core and are re-exported from `./array-helpers`
// (kept in a dedicated file so the external re-export does not sit alongside the inline
// `import('./board-column')` type references above, which trips ng-packagr's compiler).
/*
* Public API Surface of board
*/
// module
/**
* Generated bundle index. Do not edit.
*/
export { BoardColumnFooterDirective, BoardColumnHeaderDirective, BoardModule, CardDragPreviewDirective, CardPlaceholderDirective, CardTemplateDirective, ColumnDragPreviewDirective, ColumnPlaceholderDirective, HubBoardComponent, InvertColorPipe };
//# sourceMappingURL=ng-hub-ui-board.mjs.map