@brickclay-org/ui 0.1.92 → 0.1.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14078,165 +14078,173 @@ const BK_TABLE = [
14078
14078
  ];
14079
14079
 
14080
14080
  /**
14081
- * Content-agnostic kanban board: column layout, card positioning and
14082
- * drag-and-drop, and nothing about what a card contains or what a click on
14083
- * one should do.
14081
+ * Kanban board: column layout, item positioning and drag-and-drop, add and
14082
+ * remove, and nothing about what an item's *body* renders as beyond the
14083
+ * fixed `id` / `label` / `sortOrder` / `parentId` every `ColumnItem` carries.
14084
14084
  *
14085
- * The board never renders a card's insides itself `cardTemplate` does that,
14086
- * receiving the card and its column as context:
14085
+ * Columns own their items a single nested tree is the source of truth:
14087
14086
  *
14088
14087
  * <bk-kanban
14089
- * [columns]="columns"
14090
- * [cards]="cards()"
14088
+ * [columns]="columns()"
14091
14089
  * [cardTemplate]="card"
14092
- * (cardMoved)="onCardMoved($event)"
14093
- * (cardClicked)="onCardClicked($event)"
14090
+ * [showAddItem]="true"
14091
+ * [showRemoveItem]="true"
14092
+ * (columnsChanged)="onColumnsChanged($event)"
14093
+ * (itemClicked)="onItemClicked($event)"
14094
14094
  * ></bk-kanban>
14095
14095
  *
14096
14096
  * <ng-template #card let-card let-column="column">
14097
14097
  * <div [bkKanbanClickTarget]="'hours'">{{ card.hours }}h</div>
14098
- * {{ card.title }}
14098
+ * {{ card.label }}
14099
14099
  * </ng-template>
14100
14100
  *
14101
- * That is deliberate, not an oversight — see the package spec's Design
14102
- * Principles. Three consequences fall out of it directly:
14101
+ * Three consequences fall out of that directly:
14103
14102
  *
14104
- * - **Data ownership stays with the caller.** A move is reported through
14105
- * `cardMoved`, not applied to some copy of `cards` this component keeps to
14106
- * itself. Internally it *does* mutate the card objects it was handed
14107
- * (`columnIdField`'s field, and an `order` field used to remember position
14108
- * within a column) so the board redraws in the right place immediately,
14109
- * exactly the way `bk-grid`'s own row-drag mutates `result` in place and
14110
- * renumbers `sortOrder` — but persistence, and reverting the view if a
14111
- * save fails, are the host's job. Reverting is just handing back a
14112
- * `cards` array (or updated objects) that reflect the pre-move state; nothing
14103
+ * - **Data ownership stays with the caller.** Every operation is reported
14104
+ * through `columnsChanged` (only the column(s) actually affected, each
14105
+ * with its complete current `items` column reordering aside, see
14106
+ * `columnReorderPayload`), not applied to some copy of `columns` this
14107
+ * component keeps to itself. Internally it *does* mutate the column/item
14108
+ * objects it was handed (`items`, `sortOrder`, `parentId`) so the board
14109
+ * redraws in the right place immediately — but persistence, and reverting
14110
+ * the view if a save fails, are the host's job. Reverting is just handing
14111
+ * back a `columns` array that reflects the pre-operation state; nothing
14113
14112
  * further to undo on this end.
14114
- * - **Clicks are reported, never acted on.** `cardClicked` says which card and
14115
- * which named region (see `[bkKanbanClickTarget]`); opening a popup or
14113
+ * - **A cross-column move commits immediately by default** same as a
14114
+ * same-column reorder, nothing to bind. Turn on
14115
+ * `validateMove` and that flips: dragging an item onto a
14116
+ * different column then doesn't touch `columns()` at all until the
14117
+ * consumer answers `itemMoveBetweenColumns` — see that event's doc comment
14118
+ * and `onItemDrop` below for the full flow.
14119
+ * - **Clicks are reported, never acted on.** `itemClicked` says which item
14120
+ * and which named region (see `[bkKanbanClickTarget]`); opening a popup or
14116
14121
  * doing nothing at all is entirely up to whoever is listening.
14117
- * - **A card's own shape is never inspected**, beyond the two fields above.
14118
- * Tags, hours, avatars, due dates — the board doesn't know they exist.
14122
+ * - **An item's own shape past the four required fields is never
14123
+ * inspected.** Tags, hours, avatars, due dates — the board doesn't know
14124
+ * they exist. `label` is the one exception: the board reads it for the
14125
+ * built-in add/remove buttons' aria-labels and as the default label an
14126
+ * add gets when no custom `columnHeaderActionsTemplate` supplies one.
14119
14127
  */
14120
14128
  class BkKanban {
14121
14129
  /* ================= Inputs ================= */
14122
- /** Column/status definitions. Unlimited — the board renders whatever it's given. */
14130
+ /** Columns, each owning its own `items` — the single source of truth.
14131
+ * Unlimited columns, unlimited items per column; the board renders
14132
+ * whatever it's given. Mutated in place on every drag/add/remove — see
14133
+ * the component doc. */
14123
14134
  columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
14124
- /** Flat card list. Grouped into columns via `columnIdField`, ordered within
14125
- * a column by an `order` field when the cards carry one, otherwise left in
14126
- * the sequence they appear here. */
14127
- cards = input.required(...(ngDevMode ? [{ debugName: "cards" }] : []));
14128
- /** Renders a card's body. Receives `{ $implicit: card, card, column, index }`. */
14135
+ /** Renders an item's body. Receives `{ $implicit: card, card, column,
14136
+ * index, removeItem }`. */
14129
14137
  cardTemplate = input.required(...(ngDevMode ? [{ debugName: "cardTemplate" }] : []));
14130
- /** Field name, or a resolver function, that gives a card's column id.
14131
- *
14132
- * A field name can be both read and written, so a cross-column drop
14133
- * updates it directly. A resolver function is read-only the board still
14134
- * reports the move via `cardMoved`, but leaves updating the source data to
14135
- * the host, since there's nowhere on a function to write the new value. */
14136
- columnIdField = input('columnId', ...(ngDevMode ? [{ debugName: "columnIdField" }] : []));
14137
- dragEnabled = input(true, ...(ngDevMode ? [{ debugName: "dragEnabled" }] : []));
14138
- dragScope = input('both', ...(ngDevMode ? [{ debugName: "dragScope" }] : []));
14138
+ /** How far (and whether at all) a card can be dragged — see
14139
+ * `BkKanbanMovementRestriction`. One control instead of a separate
14140
+ * on/off switch plus a restriction, so there's nothing to keep in sync
14141
+ * between two inputs: `'none'` is what used to be `dragEnabled: false`. */
14142
+ movementRestriction = input('both', ...(ngDevMode ? [{ debugName: "movementRestriction" }] : []));
14139
14143
  wipLimitBehavior = input('warn', ...(ngDevMode ? [{ debugName: "wipLimitBehavior" }] : []));
14140
14144
  /** Lets columns themselves be dragged into a new order, via their header
14141
- * (`.bk-kanban-column-header` is the drag handle — a card's own drag
14145
+ * (`.bk-kanban-column-header` is the drag handle — an item's own drag
14142
14146
  * keeps working independently, since it's a separate, unconnected drop
14143
14147
  * list one level down). Off by default: reordering the board's own
14144
- * columns is a bigger commitment than reordering cards within it, and not
14148
+ * columns is a bigger commitment than reordering items within it, and not
14145
14149
  * every consumer wants a "To do / In progress / Done" pipeline to be
14146
- * rearrangeable. `columns` is mutated in place the same way `cards` is
14147
- * (see the component doc) persisting the new order, if it needs to be,
14148
- * is the host's job, done from `columnMoved`. */
14149
- columnDragEnabled = input(false, ...(ngDevMode ? [{ debugName: "columnDragEnabled" }] : []));
14150
+ * rearrangeable. */
14151
+ dragColumn = input(false, ...(ngDevMode ? [{ debugName: "dragColumn" }] : []));
14152
+ /** How much a column-reorder `columnsChanged` emission carries — see
14153
+ * `BkKanbanColumnReorderPayload`. Only affects column reordering; every
14154
+ * other operation always emits complete columns regardless. */
14155
+ columnReorderPayload = input('complete', ...(ngDevMode ? [{ debugName: "columnReorderPayload" }] : []));
14156
+ /** Off by default: a cross-column drop commits immediately, exactly like
14157
+ * a same-column reorder — `itemMoveBetweenColumns` never fires, and
14158
+ * there's nothing to bind. Turn this on to gate cross-column moves behind
14159
+ * that event instead — see its own doc comment and `onItemDrop` for the
14160
+ * full validate-before-commit flow this switches on. */
14161
+ validateMove = input(false, ...(ngDevMode ? [{ debugName: "validateMove" }] : []));
14150
14162
  /** See `BkKanbanColumnScrollMode`. */
14151
- columnScrollMode = input('body', ...(ngDevMode ? [{ debugName: "columnScrollMode" }] : []));
14152
- /** Extra class(es) appended to the board's own wrapper — `.bk-kanban-board`
14153
- * in `kanban.html`, the element that directly contains the scrollable
14154
- * region holding every column. Same idea as `columnClass`/`colHeaderClass`
14155
- * one level down: it only adds classes, nothing here replaces bk-kanban's
14156
- * own layout.
14163
+ columnScrollMode = input('column', ...(ngDevMode ? [{ debugName: "columnScrollMode" }] : []));
14164
+ /** Extra class(es) appended to the board's own wrapper — the element that
14165
+ * directly contains the scrollable region holding every column. Same
14166
+ * idea as `columnClass`/`colHeaderClass` one level down: it only adds
14167
+ * classes, nothing here replaces bk-kanban's own layout.
14157
14168
  *
14158
- * Deliberately kept off the *scrolling* element itself
14159
- * (`.bk-kanban`, one level further in): a `padding` or `border` in here
14160
- * would otherwise become part of what has to be scrolled past, and change
14161
- * where the scrollbar itself ends up sitting relative to the columns —
14162
- * this wrapper exists specifically so `boardClass` can decorate (a
14163
- * background, a border, padding around the whole board) without ever
14164
- * touching the box CDK and `columnScrollMode` are actually scrolling. */
14169
+ * Deliberately kept off the *scrolling* element itself: a `padding` or
14170
+ * `border` in here would otherwise become part of what has to be
14171
+ * scrolled past, and change where the scrollbar itself ends up sitting
14172
+ * relative to the columns this wrapper exists specifically so
14173
+ * `boardClass` can decorate (a background, a border, padding around the
14174
+ * whole board) without ever touching the box CDK and `columnScrollMode`
14175
+ * are actually scrolling. */
14165
14176
  boardClass = input('', ...(ngDevMode ? [{ debugName: "boardClass" }] : []));
14166
- /** Identity for `@for`'s `track`. Defaults to position in the rendered column. */
14177
+ /** Identity for `@for`'s `track`. Defaults to `card.id` every
14178
+ * `ColumnItem` is guaranteed to have one. */
14167
14179
  trackBy = input(null, ...(ngDevMode ? [{ debugName: "trackBy" }] : []));
14168
- /** Rendered in a fixed slot in every column's header e.g. an "add card"
14169
- * button. Receives `BkKanbanColumnActionsContext`. The board owns where it
14170
- * sits; the consumer owns what it does. */
14180
+ /** Rendered in a fixed slot in every column's header, alongside the
14181
+ * built-in add button when `showAddItem` is on. Receives
14182
+ * `BkKanbanColumnActionsContext` its `addItem` is the same logic the
14183
+ * built-in button calls, so a fully custom header action can drive it
14184
+ * too instead of reimplementing id generation / limit checks. */
14171
14185
  columnHeaderActionsTemplate = input(null, ...(ngDevMode ? [{ debugName: "columnHeaderActionsTemplate" }] : []));
14172
- /** Overrides the built-in "No cards" placeholder for an empty column.
14173
- * Receives `BkKanbanEmptyStateContext`. Takes priority over both
14174
- * `noDataText` and a column's own `noDataText` — see `emptyStateText`. */
14186
+ /** Overrides the built-in empty-column placeholder. Receives
14187
+ * `BkKanbanEmptyStateContext`. Takes priority over both `emptyMessage`
14188
+ * and a column's own `emptyMessage`. */
14175
14189
  emptyStateTemplate = input(null, ...(ngDevMode ? [{ debugName: "emptyStateTemplate" }] : []));
14176
- /** Board-wide placeholder text for an empty column, used whenever a column
14177
- * doesn't set its own `noDataText`. Only read when `emptyStateTemplate`
14178
- * isn't supplied — see `emptyStateText`. */
14179
- noDataText = input('No cards', ...(ngDevMode ? [{ debugName: "noDataText" }] : []));
14180
- /** Whether a column flags an over-limit drop while still dragging (`'live'`)
14181
- * or only once the drop is attempted (`'on-drop'`). See the doc block on
14182
- * `isLiveOverLimit` for how the two relate to `wipLimitBehavior`. */
14190
+ /** Board-wide placeholder text for an empty column, used whenever a
14191
+ * column doesn't set its own `emptyMessage`. Only read when
14192
+ * `emptyStateTemplate` isn't supplied. */
14193
+ emptyMessage = input('No cards', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : []));
14194
+ /** Whether a column flags an over-limit drop while still dragging
14195
+ * (`'live'`) or only once the drop is attempted (`'on-drop'`). */
14183
14196
  dragPreviewMode = input('live', ...(ngDevMode ? [{ debugName: "dragPreviewMode" }] : []));
14197
+ /** Shows a built-in "+" button in every column's header (unless that
14198
+ * column's own `allowAdd` is `false`) when true. It calls the same
14199
+ * `addItem` logic exposed to `columnHeaderActionsTemplate` — with a
14200
+ * default `{ label: 'New item' }` — so out-of-the-box adding works with
14201
+ * zero template configuration. Renders alongside, not instead of,
14202
+ * `columnHeaderActionsTemplate` when both are supplied. */
14203
+ showAddItem = input(false, ...(ngDevMode ? [{ debugName: "showAddItem" }] : []));
14204
+ /** Shows a built-in remove button on every card (unless that card's
14205
+ * column has `allowRemove: false`) when true — board-owned chrome, not
14206
+ * part of `cardTemplate`, so click/drag isolation is guaranteed by the
14207
+ * board itself rather than left to every consumer's own template to get
14208
+ * right. */
14209
+ showRemoveItem = input(false, ...(ngDevMode ? [{ debugName: "showRemoveItem" }] : []));
14184
14210
  /* ================= Outputs ================= */
14185
- cardMoved = output();
14186
- cardClicked = output();
14211
+ /** Fires after any operation that changes the board. See
14212
+ * `BkKanbanColumnsChangedEvent`'s doc comment for how to tell which
14213
+ * operation happened from the metadata field a returned column carries. */
14214
+ columnsChanged = output();
14187
14215
  columnLimitExceeded = output();
14188
- /** A column reorder completed — see `columnDragEnabled`. */
14189
- columnMoved = output();
14216
+ itemClicked = output();
14217
+ /** Fires for a cross-column drag before any state changes — only when
14218
+ * `validateMove` is on; see the event's own doc comment
14219
+ * and `onItemDrop`. Must be bound (and resolved) once that's on, or
14220
+ * cross-column drag-and-drop stops having any effect. */
14221
+ itemMoveBetweenColumns = output();
14190
14222
  /** Reserved — see `BkKanbanColumnActionsContext.trigger`. Most consumers
14191
14223
  * bind a click handler directly on their own header-actions template and
14192
14224
  * never reach for this. */
14193
14225
  columnActionTriggered = output();
14194
- /* ================= Grouping ================= */
14226
+ /* ================= Board state ================= */
14195
14227
  /**
14196
- * Bumped after every in-place mutation (`setColumnId`, `renumber`) so
14197
- * `grouped` re-runs even though neither `cards()`'s array identity nor any
14198
- * card's object identity changed — the same trick as the table demo's
14199
- * `pgSelectionTick`, needed for exactly the same reason: signals only
14200
- * notice a new reference, and mutating fields in place is what lets a drop
14201
- * redraw in the right place without waiting on the host to hand back a
14202
- * whole new array.
14228
+ * Bumped after every in-place mutation so the template re-reads
14229
+ * `boardColumns()` even though neither `columns()`'s own array identity
14230
+ * nor any column/item's object identity necessarily changed — mutating
14231
+ * fields in place is what lets a drop/add/remove redraw in the right
14232
+ * place without waiting on the host to hand back a whole new array.
14203
14233
  */
14204
14234
  structureTick = signal(0, ...(ngDevMode ? [{ debugName: "structureTick" }] : []));
14205
- /** Cards grouped by column id, each group in `order` order when present
14206
- * (falling back to their sequence in `cards()` otherwise). Cards whose
14207
- * resolved column id matches no known column are dropped silently rather
14208
- * than rendered nowhere. */
14209
- grouped = computed(() => {
14235
+ /** What the template actually iterates a signal that re-evaluates on
14236
+ * every `structureTick` bump, so in-place mutations to `columns()`'s own
14237
+ * items/sortOrder are always picked up. */
14238
+ boardColumns = computed(() => {
14210
14239
  this.structureTick();
14211
- const map = new Map();
14212
- for (const column of this.columns())
14213
- map.set(column.id, []);
14214
- for (const card of this.cards()) {
14215
- const list = map.get(this.resolveColumnId(card));
14216
- list?.push(card);
14217
- }
14218
- for (const list of map.values()) {
14219
- if (list.some((c) => this.cardOrder(c) !== null)) {
14220
- list.sort((a, b) => (this.cardOrder(a) ?? 0) - (this.cardOrder(b) ?? 0));
14221
- }
14222
- }
14223
- return map;
14224
- }, ...(ngDevMode ? [{ debugName: "grouped" }] : []));
14225
- /** The cards to render for one column, in display order. */
14226
- cardsFor(columnId) {
14227
- return this.grouped().get(columnId) ?? [];
14228
- }
14229
- resolveColumnId(card) {
14230
- const field = this.columnIdField();
14231
- return typeof field === 'function' ? field(card) : card[field];
14232
- }
14233
- cardOrder(card) {
14234
- const value = card['order'];
14235
- return typeof value === 'number' ? value : null;
14236
- }
14240
+ return this.columns();
14241
+ }, ...(ngDevMode ? [{ debugName: "boardColumns" }] : []));
14237
14242
  resolveTrackBy(card, index) {
14238
14243
  const fn = this.trackBy();
14239
- return fn ? fn(card) : index;
14244
+ return fn ? fn(card) : card.id;
14245
+ }
14246
+ columnById(id) {
14247
+ return this.boardColumns().find((c) => c.id === id);
14240
14248
  }
14241
14249
  /* ================= Column-level view state ================= */
14242
14250
  /** Column-header count badge, flagged once the column is over its limit —
@@ -14244,18 +14252,29 @@ class BkKanban {
14244
14252
  * column get there and `'none'` treats the number as pure information. */
14245
14253
  isColumnOverLimit(column) {
14246
14254
  return (this.wipLimitBehavior() === 'warn' &&
14247
- column.wipLimit != null &&
14248
- this.cardsFor(column.id).length > column.wipLimit);
14255
+ column.limit != null &&
14256
+ column.items.length > column.limit);
14249
14257
  }
14250
14258
  /** "3" with no limit, "3 / 5" with one — the header count badge's label. */
14251
14259
  columnCountLabel(column) {
14252
- const count = this.cardsFor(column.id).length;
14253
- return column.wipLimit != null ? `${count} / ${column.wipLimit}` : `${count}`;
14260
+ return column.limit != null ? `${column.items.length} / ${column.limit}` : `${column.items.length}`;
14261
+ }
14262
+ /** Whether the built-in "+" button should render for this column —
14263
+ * `showAddItem` board-wide, unless this column opted itself out. */
14264
+ showAddButton(column) {
14265
+ return this.showAddItem() && column.allowAdd !== false;
14266
+ }
14267
+ /** Whether the built-in remove button should render for this card's
14268
+ * column — `showRemoveItem` board-wide, unless this column opted
14269
+ * itself out. */
14270
+ showRemoveButton(column) {
14271
+ return this.showRemoveItem() && column.allowRemove !== false;
14254
14272
  }
14255
14273
  columnActionsContext(column) {
14256
14274
  return {
14257
14275
  $implicit: column,
14258
14276
  column,
14277
+ addItem: (item) => this.addItem(column.id, item),
14259
14278
  trigger: () => this.columnActionTriggered.emit({ columnId: column.id })
14260
14279
  };
14261
14280
  }
@@ -14263,49 +14282,65 @@ class BkKanban {
14263
14282
  return { $implicit: column, column };
14264
14283
  }
14265
14284
  /** Text for the built-in empty-column placeholder: a column's own
14266
- * `noDataText` when it sets one, else the board-wide `noDataText` input.
14267
- * Only consulted when `emptyStateTemplate` isn't supplied — the template
14268
- * wins outright once given, exactly like `emptyStateContext` itself. */
14269
- emptyStateText(column) {
14270
- return column.noDataText ?? this.noDataText();
14285
+ * `emptyMessage` when it sets one, else the board-wide `emptyMessage`
14286
+ * input. Only consulted when `emptyStateTemplate` isn't supplied. */
14287
+ emptyMessageFor(column) {
14288
+ return column.emptyMessage ?? this.emptyMessage();
14271
14289
  }
14272
14290
  cardContext(card, column, index) {
14273
- return { $implicit: card, card, column, index };
14291
+ return {
14292
+ $implicit: card,
14293
+ card,
14294
+ column,
14295
+ index,
14296
+ removeItem: () => this.removeItem(column.id, card.id)
14297
+ };
14274
14298
  }
14275
- /** Which columns a column's drop list connects to, per `dragScope`. */
14299
+ /** Whether any card dragging is active at all false only for
14300
+ * `movementRestriction: 'none'`. Drives both the column body's own
14301
+ * `cdkDropListDisabled` and each card's own `cdkDragDisabled`. */
14302
+ dragActive = computed(() => this.movementRestriction() !== 'none', ...(ngDevMode ? [{ debugName: "dragActive" }] : []));
14303
+ /** Which columns a column's drop list connects to, per `movementRestriction`. */
14276
14304
  connectedListIds(columnId) {
14277
- if (!this.dragEnabled() || this.dragScope() === 'same')
14305
+ const restriction = this.movementRestriction();
14306
+ if (restriction === 'none' || restriction === 'within')
14278
14307
  return [];
14279
- return this.columns()
14308
+ return this.boardColumns()
14280
14309
  .filter((c) => c.id !== columnId)
14281
14310
  .map((c) => c.id);
14282
14311
  }
14283
- /** `'cross'` means *only* moves between columns — reordering within one has
14284
- * to be switched off at the list level, since a scoped-out `connectedTo`
14285
- * alone still leaves the list free to sort itself. */
14286
- sortingDisabled = computed(() => this.dragScope() === 'cross', ...(ngDevMode ? [{ debugName: "sortingDisabled" }] : []));
14312
+ /** `'between'` means *only* moves between columns — reordering
14313
+ * within one has to be switched off at the list level, since a
14314
+ * scoped-out `connectedTo` alone still leaves the list free to sort
14315
+ * itself. `'none'` doesn't need to appear here too: `cdkDropListDisabled`
14316
+ * (bound to `dragActive`) already switches the whole list off, so whether
14317
+ * sorting specifically is "disabled" on top of that is moot. */
14318
+ sortingDisabled = computed(() => this.movementRestriction() === 'between', ...(ngDevMode ? [{ debugName: "sortingDisabled" }] : []));
14287
14319
  /* ================= Column-level drag (reordering columns) ================= */
14288
14320
  /**
14289
- * `columns` is reordered in place — same trick as `cards` (see the
14321
+ * `columns` is reordered in place — same trick as items (see the
14290
14322
  * component doc): the array reference the host handed in is mutated
14291
- * directly (`moveItemInArray`) rather than swapped for a new one, so the
14292
- * next render just reflects the new order. There's no `structureTick`-style
14293
- * signal to bump here the way card moves need one nothing derives
14294
- * columns through a `computed()` the way `cardsFor` derives through
14295
- * `grouped`; the template reads `columns()` directly in its `@for`, so the
14296
- * drop event's own change detection pass is enough.
14323
+ * directly (`moveItemInArray`) rather than swapped for a new one.
14324
+ * `sortOrder` is then renumbered for the whole array, and unlike every
14325
+ * other operation, which reports only the column(s) actually affected
14326
+ * *every* column in the board is reported here, in its resulting order,
14327
+ * shaped per `columnReorderPayload`: consumers persisting a full column
14328
+ * order need to see the whole list, not just the entries that happened to
14329
+ * shift.
14297
14330
  */
14298
14331
  onColumnDrop(event) {
14299
- if (!this.columnDragEnabled() || event.previousIndex === event.currentIndex)
14332
+ if (!this.dragColumn() || event.previousIndex === event.currentIndex)
14300
14333
  return;
14301
14334
  const columns = this.columns();
14302
- const moved = columns[event.previousIndex];
14303
14335
  moveItemInArray(columns, event.previousIndex, event.currentIndex);
14304
- this.columnMoved.emit({
14305
- columnId: moved.id,
14306
- fromIndex: event.previousIndex,
14307
- toIndex: event.currentIndex
14336
+ columns.forEach((c, i) => {
14337
+ c.sortOrder = i + 1;
14308
14338
  });
14339
+ this.structureTick.update((n) => n + 1);
14340
+ const payload = this.columnReorderPayload() === 'summary'
14341
+ ? columns.map((c) => ({ id: c.id, sortOrder: c.sortOrder }))
14342
+ : columns.map((c) => ({ ...c }));
14343
+ this.columnsChanged.emit({ columns: payload });
14309
14344
  }
14310
14345
  /* ================= Drag state (for the live WIP preview) ================= */
14311
14346
  dragSourceColumnId = signal(null, ...(ngDevMode ? [{ debugName: "dragSourceColumnId" }] : []));
@@ -14318,19 +14353,19 @@ class BkKanban {
14318
14353
  this.hoveredOverLimitColumnId.set(null);
14319
14354
  }
14320
14355
  /**
14321
- * Live WIP-limit preview (§12 of the spec): as the dragged card enters a
14322
- * column, work out whether *landing* here would exceed its `wipLimit` and
14323
- * flag it immediately — before release, and independent of
14324
- * `wipLimitBehavior`, which only governs what happens once the card is
14325
- * actually dropped. A card re-entering its own source column doesn't grow
14326
- * that column's count, so only a genuine cross-column entry counts as +1.
14356
+ * Live WIP-limit preview: as the dragged item enters a column, work out
14357
+ * whether *landing* here would exceed its `limit` and flag it immediately
14358
+ * — before release, and independent of `wipLimitBehavior`, which only
14359
+ * governs what happens once the item is actually dropped. An item
14360
+ * re-entering its own source column doesn't grow that column's count, so
14361
+ * only a genuine cross-column entry counts as +1.
14327
14362
  */
14328
14363
  onListEntered(event, column) {
14329
- if (this.dragPreviewMode() !== 'live' || column.wipLimit == null)
14364
+ if (this.dragPreviewMode() !== 'live' || column.limit == null)
14330
14365
  return;
14331
14366
  const enteringFromElsewhere = this.dragSourceColumnId() !== column.id;
14332
14367
  const wouldBeCount = event.container.data.length + (enteringFromElsewhere ? 1 : 0);
14333
- this.hoveredOverLimitColumnId.set(wouldBeCount > column.wipLimit ? column.id : null);
14368
+ this.hoveredOverLimitColumnId.set(wouldBeCount > column.limit ? column.id : null);
14334
14369
  }
14335
14370
  onListExited(column) {
14336
14371
  if (this.hoveredOverLimitColumnId() === column.id)
@@ -14339,87 +14374,233 @@ class BkKanban {
14339
14374
  isLiveOverLimit(column) {
14340
14375
  return this.hoveredOverLimitColumnId() === column.id;
14341
14376
  }
14342
- /* ================= Drop handling ================= */
14343
- onDrop(event, column) {
14377
+ /* ================= Item drop handling ================= */
14378
+ renumber(items) {
14379
+ items.forEach((item, index) => {
14380
+ item.sortOrder = index + 1;
14381
+ });
14382
+ }
14383
+ /**
14384
+ * Same-column reorder commits immediately (nothing to validate — the
14385
+ * column's own membership never changes) and reports the one affected
14386
+ * column with its complete, re-ordered `items`. A genuine cross-column
14387
+ * drop is a different story: see the second half of this method and
14388
+ * `itemMoveBetweenColumns`'s own doc comment for why it does *not*
14389
+ * mutate anything here.
14390
+ */
14391
+ onItemDrop(event, column) {
14344
14392
  this.dragSourceColumnId.set(null);
14345
14393
  this.hoveredOverLimitColumnId.set(null);
14346
- if (!this.dragEnabled())
14394
+ if (!this.dragActive())
14347
14395
  return;
14348
14396
  const fromColumnId = event.previousContainer.id;
14349
14397
  const toColumnId = event.container.id;
14350
- const card = event.item.data;
14398
+ const item = event.item.data;
14351
14399
  if (fromColumnId === toColumnId) {
14352
14400
  if (event.previousIndex === event.currentIndex)
14353
14401
  return;
14354
- const list = this.cardsFor(toColumnId);
14355
- moveItemInArray(list, event.previousIndex, event.currentIndex);
14356
- this.renumber(list);
14402
+ const items = column.items;
14403
+ moveItemInArray(items, event.previousIndex, event.currentIndex);
14404
+ this.renumber(items);
14357
14405
  this.structureTick.update((n) => n + 1);
14358
- this.cardMoved.emit({ card, fromColumnId, toColumnId, newIndex: event.currentIndex });
14406
+ this.columnsChanged.emit({ columns: [{ ...column }] });
14359
14407
  return;
14360
14408
  }
14361
- const targetList = this.cardsFor(toColumnId);
14362
- const attemptedCount = targetList.length + 1;
14363
- const wipLimit = column.wipLimit;
14364
- const wouldExceed = wipLimit != null && attemptedCount > wipLimit;
14409
+ const fromColumn = this.columnById(fromColumnId);
14410
+ const toColumn = this.columnById(toColumnId) ?? column;
14411
+ if (!fromColumn)
14412
+ return;
14413
+ const attemptedCount = toColumn.items.length + 1;
14414
+ const limit = toColumn.limit;
14415
+ const wouldExceed = limit != null && attemptedCount > limit;
14365
14416
  if (wouldExceed) {
14366
- this.columnLimitExceeded.emit({ columnId: toColumnId, wipLimit: wipLimit, attemptedCount });
14417
+ this.columnLimitExceeded.emit({ columnId: toColumnId, limit: limit, attemptedCount });
14367
14418
  }
14368
14419
  if (wouldExceed && this.wipLimitBehavior() === 'block') {
14369
- // Rejected: nothing below is mutated, so the next render finds the card
14370
- // still exactly where `cards()` says it is — the same "snap back" any
14371
- // cancelled CDK drag shows, not a bespoke undo.
14372
- return;
14373
- }
14374
- const sourceList = this.cardsFor(fromColumnId);
14375
- const fromIndex = sourceList.indexOf(card);
14420
+ // Rejected before validation is even asked for: nothing below runs,
14421
+ // so the next render finds the item still exactly where `columns()`
14422
+ // says it is — the same "snap back" any cancelled CDK drag shows.
14423
+ return;
14424
+ }
14425
+ const targetIndex = event.currentIndex;
14426
+ // Opt-in gate: off by default, a cross-column drop commits right here,
14427
+ // same as a same-column reorder — no event, nothing to wait for.
14428
+ if (!this.validateMove()) {
14429
+ this.commitCrossColumnMove(fromColumn, toColumn, item, targetIndex);
14430
+ return;
14431
+ }
14432
+ // ---- Pre-drop validation (§5-§9, §23), only once opted into ----
14433
+ //
14434
+ // Everything needed to describe the move is worked out now, from
14435
+ // `event`'s own indices — CDK reports `currentIndex` as the intended
14436
+ // position within the destination list purely from the drag's visual
14437
+ // state, so it's already meaningful even though `toColumn.items` itself
14438
+ // hasn't been touched yet. Nothing below this point mutates board state
14439
+ // — that only happens inside `commitCrossColumnMove`, and only once
14440
+ // `resolve` is actually called. Until then (and forever, if it's never
14441
+ // called) `fromColumn`/`toColumn` are untouched, so the item simply
14442
+ // stays rendered exactly where `columns()` already says it is — "restore
14443
+ // to original position" on failure is this doing nothing, not a
14444
+ // separate undo step.
14445
+ const targetItem = toColumn.items[targetIndex] ?? toColumn.items[targetIndex - 1] ?? null;
14446
+ let settled = false;
14447
+ this.itemMoveBetweenColumns.emit({
14448
+ item,
14449
+ sourceColumn: { ...fromColumn },
14450
+ destinationColumn: { ...toColumn },
14451
+ targetItem,
14452
+ targetIndex,
14453
+ resolve: (result) => {
14454
+ if (settled)
14455
+ return;
14456
+ settled = true;
14457
+ if (result?.success) {
14458
+ this.commitCrossColumnMove(fromColumn, toColumn, item, targetIndex);
14459
+ }
14460
+ // Failure: intentionally nothing else to do — see comment above.
14461
+ }
14462
+ });
14463
+ }
14464
+ /** The actual cross-column mutation, run only once `resolve({ success:
14465
+ * true })` comes back from `itemMoveBetweenColumns` — see `onItemDrop`. */
14466
+ commitCrossColumnMove(fromColumn, toColumn, item, targetIndex) {
14467
+ const fromIndex = fromColumn.items.indexOf(item);
14376
14468
  if (fromIndex < 0)
14377
- return; // Column and card disagree about where it lives; leave state alone.
14378
- sourceList.splice(fromIndex, 1);
14379
- targetList.splice(event.currentIndex, 0, card);
14380
- this.setColumnId(card, toColumnId);
14381
- this.renumber(sourceList);
14382
- this.renumber(targetList);
14469
+ return; // Column and item disagree about where it lives; leave state alone.
14470
+ fromColumn.items.splice(fromIndex, 1);
14471
+ const insertIndex = Math.min(targetIndex, toColumn.items.length);
14472
+ toColumn.items.splice(insertIndex, 0, item);
14473
+ item.parentId = toColumn.id;
14474
+ this.renumber(fromColumn.items);
14475
+ this.renumber(toColumn.items);
14476
+ this.structureTick.update((n) => n + 1);
14477
+ this.columnsChanged.emit({
14478
+ columns: [
14479
+ { ...fromColumn, sentChildId: item.id },
14480
+ { ...toColumn, receivedChildId: item.id }
14481
+ ]
14482
+ });
14483
+ }
14484
+ /* ================= Add / remove ================= */
14485
+ generateId() {
14486
+ return typeof crypto !== 'undefined' && crypto.randomUUID
14487
+ ? crypto.randomUUID()
14488
+ : `item-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
14489
+ }
14490
+ /** Adds a new item to a column — the logic backing both the built-in "+"
14491
+ * button (`showAddItem`) and `BkKanbanColumnActionsContext.addItem`
14492
+ * for a fully custom header template. Supply at least `label`; an `id`
14493
+ * given is used as-is, otherwise one is generated. Respects
14494
+ * `wipLimitBehavior` exactly like a cross-column drop does, and does
14495
+ * nothing at all for a column with `allowAdd: false` or an empty/missing
14496
+ * `label` — see §11. */
14497
+ addItem(columnId, data) {
14498
+ const column = this.columnById(columnId);
14499
+ if (!column || column.allowAdd === false)
14500
+ return;
14501
+ if (!data?.label || !data.label.trim())
14502
+ return;
14503
+ const attemptedCount = column.items.length + 1;
14504
+ const limit = column.limit;
14505
+ const wouldExceed = limit != null && attemptedCount > limit;
14506
+ if (wouldExceed) {
14507
+ this.columnLimitExceeded.emit({ columnId, limit: limit, attemptedCount });
14508
+ }
14509
+ if (wouldExceed && this.wipLimitBehavior() === 'block')
14510
+ return;
14511
+ const id = data.id ?? this.generateId();
14512
+ const item = { ...data, id, sortOrder: column.items.length + 1, parentId: columnId };
14513
+ column.items.push(item);
14383
14514
  this.structureTick.update((n) => n + 1);
14384
- this.cardMoved.emit({ card, fromColumnId, toColumnId, newIndex: event.currentIndex });
14385
- }
14386
- setColumnId(card, columnId) {
14387
- const field = this.columnIdField();
14388
- // A resolver function has nowhere to write back to — the host owns that
14389
- // mapping and is expected to apply the move from `cardMoved` itself.
14390
- if (typeof field === 'string')
14391
- card[field] = columnId;
14392
- }
14393
- renumber(list) {
14394
- list.forEach((card, index) => {
14395
- card['order'] = index;
14515
+ this.columnsChanged.emit({
14516
+ columns: [{ ...column, newAddedItemId: id, newItemCreated: true }]
14396
14517
  });
14397
14518
  }
14398
- /* ================= Click handling (§5) ================= */
14519
+ /** Removes an item from a column — the logic backing both the built-in
14520
+ * remove button (`showRemoveItem`) and `BkKanbanCardContext.removeItem`
14521
+ * for a fully custom card body. Does nothing for a column with
14522
+ * `allowRemove: false`, or if the item isn't actually there. */
14523
+ removeItem(columnId, itemId) {
14524
+ const column = this.columnById(columnId);
14525
+ if (!column || column.allowRemove === false)
14526
+ return;
14527
+ const index = column.items.findIndex((i) => i.id === itemId);
14528
+ if (index < 0)
14529
+ return;
14530
+ column.items.splice(index, 1);
14531
+ this.renumber(column.items);
14532
+ this.structureTick.update((n) => n + 1);
14533
+ this.columnsChanged.emit({ columns: [{ ...column, removedItemId: itemId }] });
14534
+ }
14535
+ onAddButtonClick(column) {
14536
+ this.addItem(column.id, { label: 'New item' });
14537
+ }
14538
+ /** The remove button's own click handler. It's a `bk-icon-button`, so its
14539
+ * own `(clicked)` output only ever carries a boolean, not the originating
14540
+ * event — isolation happens entirely in the template instead, via native
14541
+ * `mousedown`/`pointerdown`/`click` listeners bound directly on the
14542
+ * `<bk-icon-button>` host: `mousedown`/`pointerdown` are stopped before
14543
+ * CDK's drag-start detection (which listens on the card itself, an
14544
+ * ancestor of this button) ever sees the press, and stopping `click` too
14545
+ * keeps it from also reaching `onItemClick` on the card underneath. See
14546
+ * §18/§12 of the spec this shipped against. */
14547
+ onRemoveButtonClick(column, card) {
14548
+ this.removeItem(column.id, card.id);
14549
+ }
14550
+ /* ================= Remove button hover (icon swap) =================
14551
+ *
14552
+ * `bk-icon-button`'s own stylesheet defines no real `:hover` state (only
14553
+ * `active:`/`focus-visible:`) — the pattern this codebase already uses
14554
+ * instead, for the same "×" affordance elsewhere (see the fleet module's
14555
+ * own close button), is to swap `variant` (from `'none'`, transparent, to
14556
+ * `'primary'`, solid) *and* the icon asset itself (a black icon in the
14557
+ * `'none'` state needs to become the white variant once the background
14558
+ * goes solid) on `(hovered)`, rather than reaching for a CSS override.
14559
+ *
14560
+ * Tracked per item id, not a single shared boolean: there's one remove
14561
+ * button per card, all sharing this same board instance, so "which one"
14562
+ * has to be part of the state. The add button stays a plain `bk-button`
14563
+ * (see kanban.html) and doesn't need this.
14564
+ */
14565
+ hoveredRemoveItemId = signal(null, ...(ngDevMode ? [{ debugName: "hoveredRemoveItemId" }] : []));
14566
+ removeButtonVariant(card) {
14567
+ return this.hoveredRemoveItemId() === card.id ? 'primary' : 'none';
14568
+ }
14569
+ removeButtonIcon(card) {
14570
+ return this.hoveredRemoveItemId() === card.id
14571
+ ? '../../../../assets/images/icons/global/popup-cross-white.svg'
14572
+ : '../../../../assets/images/icons/global/popup-cross.svg';
14573
+ }
14574
+ onRemoveButtonHover(card, hovered) {
14575
+ this.hoveredRemoveItemId.set(hovered ? card.id : null);
14576
+ }
14577
+ /* ================= Click handling ================= */
14399
14578
  /**
14400
14579
  * One listener per card resolves both the whole-card click and every
14401
14580
  * `[bkKanbanClickTarget]` region: `closest()` from the actual event target
14402
14581
  * finds the nearest marked ancestor within this card, or there isn't one
14403
14582
  * and it's a plain card click. Either way this fires exactly once — never
14404
- * once for the region and again for the card underneath it.
14583
+ * once for the region and again for the card underneath it. The remove
14584
+ * button (when shown) stops its own click from ever reaching here — see
14585
+ * `onRemoveButtonClick`.
14405
14586
  */
14406
- onCardClick(event, card) {
14587
+ onItemClick(event, card) {
14407
14588
  const wrapper = event.currentTarget;
14408
14589
  const marked = event.target?.closest('[data-bk-kanban-click-target]');
14409
14590
  const target = marked && wrapper.contains(marked)
14410
14591
  ? (marked.dataset['bkKanbanClickTarget'] ?? 'card')
14411
14592
  : 'card';
14412
- this.cardClicked.emit({ card, target });
14593
+ this.itemClicked.emit({ item: card, target });
14413
14594
  }
14414
14595
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BkKanban, deps: [], target: i0.ɵɵFactoryTarget.Component });
14415
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: BkKanban, isStandalone: true, selector: "bk-kanban", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: true, transformFunction: null }, cardTemplate: { classPropertyName: "cardTemplate", publicName: "cardTemplate", isSignal: true, isRequired: true, transformFunction: null }, columnIdField: { classPropertyName: "columnIdField", publicName: "columnIdField", isSignal: true, isRequired: false, transformFunction: null }, dragEnabled: { classPropertyName: "dragEnabled", publicName: "dragEnabled", isSignal: true, isRequired: false, transformFunction: null }, dragScope: { classPropertyName: "dragScope", publicName: "dragScope", isSignal: true, isRequired: false, transformFunction: null }, wipLimitBehavior: { classPropertyName: "wipLimitBehavior", publicName: "wipLimitBehavior", isSignal: true, isRequired: false, transformFunction: null }, columnDragEnabled: { classPropertyName: "columnDragEnabled", publicName: "columnDragEnabled", isSignal: true, isRequired: false, transformFunction: null }, columnScrollMode: { classPropertyName: "columnScrollMode", publicName: "columnScrollMode", isSignal: true, isRequired: false, transformFunction: null }, boardClass: { classPropertyName: "boardClass", publicName: "boardClass", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, columnHeaderActionsTemplate: { classPropertyName: "columnHeaderActionsTemplate", publicName: "columnHeaderActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, emptyStateTemplate: { classPropertyName: "emptyStateTemplate", publicName: "emptyStateTemplate", isSignal: true, isRequired: false, transformFunction: null }, noDataText: { classPropertyName: "noDataText", publicName: "noDataText", isSignal: true, isRequired: false, transformFunction: null }, dragPreviewMode: { classPropertyName: "dragPreviewMode", publicName: "dragPreviewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cardMoved: "cardMoved", cardClicked: "cardClicked", columnLimitExceeded: "columnLimitExceeded", columnMoved: "columnMoved", columnActionTriggered: "columnActionTriggered" }, host: { classAttribute: "bk-kanban-host" }, exportAs: ["bkKanban"], ngImport: i0, template: "<div class=\"bk-kanban-board\" [class]=\"boardClass()\">\r\n <div\r\n class=\"bk-kanban\"\r\n [class.bk-kanban--scroll-wrapper]=\"columnScrollMode() === 'wrapper'\"\r\n cdkDropList\r\n cdkDropListOrientation=\"horizontal\"\r\n [cdkDropListData]=\"columns()\"\r\n [cdkDropListDisabled]=\"!columnDragEnabled()\"\r\n (cdkDropListDropped)=\"onColumnDrop($event)\"\r\n >\r\n @for (column of columns(); track column.id) {\r\n <div\r\n class=\"bk-kanban-column\"\r\n cdkDrag\r\n [cdkDragData]=\"column\"\r\n [cdkDragDisabled]=\"!columnDragEnabled()\"\r\n [class]=\"column.columnClass\"\r\n [class.bk-kanban-column-warn]=\"isColumnOverLimit(column)\"\r\n [class.bk-kanban-column-live-over]=\"isLiveOverLimit(column)\"\r\n >\r\n <div class=\"bk-kanban-column-header\" cdkDragHandle [class]=\"column.colHeaderClass\">\r\n <span class=\"bk-kanban-column-title\">{{ column.label }}</span>\r\n <bk-badge\r\n class=\"bk-kanban-column-count\"\r\n [label]=\"columnCountLabel(column)\"\r\n size=\"sm\"\r\n [color]=\"isColumnOverLimit(column) || isLiveOverLimit(column) ? 'Warning' : 'Gray'\"\r\n ></bk-badge>\r\n\r\n @if (columnHeaderActionsTemplate(); as actionsTpl) {\r\n <div class=\"bk-kanban-column-actions\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionsTpl\"\r\n [ngTemplateOutletContext]=\"columnActionsContext(column)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"bk-kanban-column-body\"\r\n cdkDropList\r\n [id]=\"column.id\"\r\n [cdkDropListData]=\"cardsFor(column.id)\"\r\n [cdkDropListConnectedTo]=\"connectedListIds(column.id)\"\r\n [cdkDropListSortingDisabled]=\"sortingDisabled()\"\r\n [cdkDropListDisabled]=\"!dragEnabled()\"\r\n (cdkDropListDropped)=\"onDrop($event, column)\"\r\n (cdkDropListEntered)=\"onListEntered($event, column)\"\r\n (cdkDropListExited)=\"onListExited(column)\"\r\n >\r\n @if (cardsFor(column.id).length === 0) {\r\n <div class=\"bk-kanban-empty\">\r\n @if (emptyStateTemplate(); as emptyTpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"emptyTpl\"\r\n [ngTemplateOutletContext]=\"emptyStateContext(column)\"\r\n ></ng-container>\r\n } @else {\r\n <span class=\"bk-kanban-empty-text\">{{ emptyStateText(column) }}</span>\r\n }\r\n </div>\r\n }\r\n\r\n @for (card of cardsFor(column.id); track resolveTrackBy(card, $index); let i = $index) {\r\n <div\r\n class=\"bk-kanban-card\"\r\n cdkDrag\r\n [cdkDragData]=\"card\"\r\n [cdkDragDisabled]=\"!dragEnabled()\"\r\n (cdkDragStarted)=\"onDragStarted(column)\"\r\n (cdkDragEnded)=\"onDragEnded()\"\r\n (click)=\"onCardClick($event, card)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardTemplate()\"\r\n [ngTemplateOutletContext]=\"cardContext(card, column, i)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".bk-kanban-host,.bk-kanban-board{display:block;height:100%}.bk-kanban{@apply flex items-stretch gap-4 w-full h-full overflow-x-auto;scroll-snap-type:x proximity}.bk-kanban--scroll-wrapper{align-items:flex-start;overflow-y:auto}.bk-kanban--scroll-wrapper .bk-kanban-column-body{overflow-y:visible}.bk-kanban-column{@apply flex flex-col shrink-0 w-[300px] max-w-[85vw] rounded-xl border border-[#EBEDF3] overflow-hidden;background:var(--bk-kanban-column-bg, #f9fafa);scroll-snap-align:start}.bk-kanban-column-header{@apply flex items-center justify-between gap-2 px-3 py-2.5;background:var(--bk-kanban-header-bg, #f9fafa);border-bottom:1px solid #ebedf3}.bk-kanban-column:not(.cdk-drag-disabled)>.bk-kanban-column-header{cursor:grab}.bk-kanban-column.cdk-drag-dragging>.bk-kanban-column-header{cursor:grabbing}.bk-kanban-column-title{@apply text-[13px] font-semibold text-[#15191E] truncate;}.bk-kanban-column-count{@apply shrink-0;}.bk-kanban-column-actions{@apply shrink-0 flex items-center;}.bk-kanban-column-body{@apply flex-1 flex flex-col gap-2 p-2.5 overflow-y-auto;min-height:96px}.bk-kanban-empty{@apply flex flex-1 flex-col items-center justify-center py-6 text-center;}.bk-kanban-empty-text{@apply text-xs font-medium text-[#78829D];}.bk-kanban-card{@apply bg-white rounded-lg border border-[#EBEDF3] p-3 cursor-pointer;box-shadow:0 1px 2px #1018280a;transition:box-shadow .15s ease,border-color .15s ease}.bk-kanban-card:hover{@apply border-[#C4CADA];box-shadow:0 2px 6px #10182814}.bk-kanban-card.cdk-drag-dragging{transition:none}.bk-kanban-column-warn{@apply border-amber-300 bg-amber-50;}.bk-kanban-column-warn .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-column-live-over{@apply border-amber-400 bg-amber-50;outline:2px dashed theme(\"colors.amber.400\");outline-offset:-2px}.bk-kanban-column-live-over .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-card.cdk-drag-preview{@apply rounded-lg;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;cursor:grabbing}.bk-kanban-card.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.6}.bk-kanban-column-body.cdk-drop-list-dragging .bk-kanban-card:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.bk-kanban-column.cdk-drag-preview{box-shadow:0 8px 10px -6px #0003,0 20px 25px -5px #00000024}.bk-kanban-column.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.5}.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-header,.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-body{visibility:hidden}.bk-kanban.cdk-drop-list-dragging .bk-kanban-column:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@media (max-width: 640px){.bk-kanban-column{width:86vw}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: BkBadge, selector: "bk-badge", inputs: ["label", "variant", "color", "size", "dot", "removable", "customClass", "customBg", "customBorder", "customText"], outputs: ["clicked"] }], encapsulation: i0.ViewEncapsulation.None });
14596
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: BkKanban, isStandalone: true, selector: "bk-kanban", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, cardTemplate: { classPropertyName: "cardTemplate", publicName: "cardTemplate", isSignal: true, isRequired: true, transformFunction: null }, movementRestriction: { classPropertyName: "movementRestriction", publicName: "movementRestriction", isSignal: true, isRequired: false, transformFunction: null }, wipLimitBehavior: { classPropertyName: "wipLimitBehavior", publicName: "wipLimitBehavior", isSignal: true, isRequired: false, transformFunction: null }, dragColumn: { classPropertyName: "dragColumn", publicName: "dragColumn", isSignal: true, isRequired: false, transformFunction: null }, columnReorderPayload: { classPropertyName: "columnReorderPayload", publicName: "columnReorderPayload", isSignal: true, isRequired: false, transformFunction: null }, validateMove: { classPropertyName: "validateMove", publicName: "validateMove", isSignal: true, isRequired: false, transformFunction: null }, columnScrollMode: { classPropertyName: "columnScrollMode", publicName: "columnScrollMode", isSignal: true, isRequired: false, transformFunction: null }, boardClass: { classPropertyName: "boardClass", publicName: "boardClass", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, columnHeaderActionsTemplate: { classPropertyName: "columnHeaderActionsTemplate", publicName: "columnHeaderActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, emptyStateTemplate: { classPropertyName: "emptyStateTemplate", publicName: "emptyStateTemplate", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, dragPreviewMode: { classPropertyName: "dragPreviewMode", publicName: "dragPreviewMode", isSignal: true, isRequired: false, transformFunction: null }, showAddItem: { classPropertyName: "showAddItem", publicName: "showAddItem", isSignal: true, isRequired: false, transformFunction: null }, showRemoveItem: { classPropertyName: "showRemoveItem", publicName: "showRemoveItem", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnsChanged: "columnsChanged", columnLimitExceeded: "columnLimitExceeded", itemClicked: "itemClicked", itemMoveBetweenColumns: "itemMoveBetweenColumns", columnActionTriggered: "columnActionTriggered" }, host: { classAttribute: "bk-kanban-host" }, exportAs: ["bkKanban"], ngImport: i0, template: "<div class=\"bk-kanban-board\" [class]=\"boardClass()\">\r\n <div\r\n class=\"bk-kanban\"\r\n [class.bk-kanban--scroll-wrapper]=\"columnScrollMode() === 'wrapper'\"\r\n cdkDropList\r\n cdkDropListOrientation=\"horizontal\"\r\n [cdkDropListData]=\"boardColumns()\"\r\n [cdkDropListDisabled]=\"!dragColumn()\"\r\n (cdkDropListDropped)=\"onColumnDrop($event)\"\r\n >\r\n @for (column of boardColumns(); track column.id) {\r\n <div\r\n class=\"bk-kanban-column\"\r\n cdkDrag\r\n [cdkDragData]=\"column\"\r\n [cdkDragDisabled]=\"!dragColumn()\"\r\n [class]=\"column.columnClass\"\r\n [class.bk-kanban-column-warn]=\"isColumnOverLimit(column)\"\r\n [class.bk-kanban-column-live-over]=\"isLiveOverLimit(column)\"\r\n >\r\n <div class=\"bk-kanban-column-header\" cdkDragHandle [class]=\"column.colHeaderClass\">\r\n <span class=\"bk-kanban-column-title\">{{ column.label }}</span>\r\n <bk-badge\r\n class=\"bk-kanban-column-count\"\r\n [label]=\"columnCountLabel(column)\"\r\n size=\"sm\"\r\n [color]=\"isColumnOverLimit(column) || isLiveOverLimit(column) ? 'Warning' : 'Gray'\"\r\n ></bk-badge>\r\n\r\n @if (columnHeaderActionsTemplate(); as actionsTpl) {\r\n <div class=\"bk-kanban-column-actions\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionsTpl\"\r\n [ngTemplateOutletContext]=\"columnActionsContext(column)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n @if (showAddButton(column)) {\r\n <bk-button\r\n class=\"bk-kanban-add-btn\"\r\n variant=\"primary\"\r\n size=\"xxsm\"\r\n [shadow]=\"false\"\r\n label=\"+\"\r\n [buttonClass]=\"'size-6 hover:bg-[#242424]'\"\r\n [attr.aria-label]=\"'Add item to ' + column.label\"\r\n (clicked)=\"onAddButtonClick(column)\"\r\n ></bk-button>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"bk-kanban-column-body\"\r\n cdkDropList\r\n [id]=\"column.id\"\r\n [cdkDropListData]=\"column.items\"\r\n [cdkDropListConnectedTo]=\"connectedListIds(column.id)\"\r\n [cdkDropListSortingDisabled]=\"sortingDisabled()\"\r\n [cdkDropListDisabled]=\"!dragActive()\"\r\n (cdkDropListDropped)=\"onItemDrop($event, column)\"\r\n (cdkDropListEntered)=\"onListEntered($event, column)\"\r\n (cdkDropListExited)=\"onListExited(column)\"\r\n >\r\n @if (column.items.length === 0) {\r\n <div class=\"bk-kanban-empty\">\r\n @if (emptyStateTemplate(); as emptyTpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"emptyTpl\"\r\n [ngTemplateOutletContext]=\"emptyStateContext(column)\"\r\n ></ng-container>\r\n } @else {\r\n <span class=\"bk-kanban-empty-text\">{{ emptyMessageFor(column) }}</span>\r\n }\r\n </div>\r\n }\r\n\r\n @for (card of column.items; track resolveTrackBy(card, $index); let i = $index) {\r\n <div\r\n class=\"bk-kanban-card\"\r\n cdkDrag\r\n [cdkDragData]=\"card\"\r\n [cdkDragDisabled]=\"!dragActive()\"\r\n (cdkDragStarted)=\"onDragStarted(column)\"\r\n (cdkDragEnded)=\"onDragEnded()\"\r\n (click)=\"onItemClick($event, card)\"\r\n >\r\n @if (showRemoveButton(column)) {\r\n <bk-icon-button\r\n class=\"bk-kanban-remove-btn\"\r\n [icon]=\"removeButtonIcon(card)\"\r\n [alt]=\"'Remove ' + card.label\"\r\n [variant]=\"removeButtonVariant(card)\"\r\n size=\"xxxsm\"\r\n (hovered)=\"onRemoveButtonHover(card, $event)\"\r\n (mousedown)=\"$event.stopPropagation()\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"$event.stopPropagation()\"\r\n (clicked)=\"onRemoveButtonClick(column, card)\"\r\n ></bk-icon-button>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardTemplate()\"\r\n [ngTemplateOutletContext]=\"cardContext(card, column, i)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".bk-kanban-host{display:block;height:100%}:where(.bk-kanban-board){display:block;height:100%}.bk-kanban{@apply flex items-stretch gap-4 w-full h-full overflow-x-auto;scroll-snap-type:x proximity}.bk-kanban--scroll-wrapper{align-items:flex-start;overflow-y:auto}.bk-kanban--scroll-wrapper .bk-kanban-column-body{overflow-y:visible}:where(.bk-kanban-column){@apply flex flex-col shrink-0 w-[300px] max-w-[85vw] rounded-xl border border-[#EBEDF3] overflow-hidden;background:var(--bk-kanban-column-bg, #f9fafa);scroll-snap-align:start}:where(.bk-kanban-column-header){@apply flex items-center justify-between gap-2 px-3 py-2.5;background:var(--bk-kanban-header-bg, #f9fafa);border-bottom:1px solid #ebedf3}.bk-kanban-column:not(.cdk-drag-disabled)>.bk-kanban-column-header{cursor:grab}.bk-kanban-column.cdk-drag-dragging>.bk-kanban-column-header{cursor:grabbing}.bk-kanban-column-title{@apply text-[13px] font-semibold text-[#15191E] truncate;}.bk-kanban-column-count{@apply shrink-0;}.bk-kanban-column-actions{@apply shrink-0 flex items-center;}.bk-kanban-column-body{@apply flex-1 flex flex-col gap-2 p-2.5 overflow-y-auto;min-height:96px}.bk-kanban-empty{@apply flex flex-1 flex-col items-center justify-center py-6 text-center;}.bk-kanban-empty-text{@apply text-xs font-medium text-[#78829D];}.bk-kanban-add-btn{@apply shrink-0;}.bk-kanban-card{@apply relative bg-white rounded-lg border border-[#EBEDF3] p-3 cursor-pointer;box-shadow:0 1px 2px #1018280a;transition:box-shadow .15s ease,border-color .15s ease}.bk-kanban-remove-btn{@apply absolute top-1.5 right-1.5 z-10;}.bk-kanban-card:hover{@apply border-[#C4CADA];box-shadow:0 2px 6px #10182814}.bk-kanban-card.cdk-drag-dragging{transition:none}.bk-kanban-column-warn{@apply border-amber-300 bg-amber-50;}.bk-kanban-column-warn .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-column-live-over{@apply border-amber-400 bg-amber-50;outline:2px dashed theme(\"colors.amber.400\");outline-offset:-2px}.bk-kanban-column-live-over .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-card.cdk-drag-preview{@apply rounded-lg;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;cursor:grabbing}.bk-kanban-card.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.6}.bk-kanban-column-body.cdk-drop-list-dragging .bk-kanban-card:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.bk-kanban-column.cdk-drag-preview{box-shadow:0 8px 10px -6px #0003,0 20px 25px -5px #00000024}.bk-kanban-column.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.5}.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-header,.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-body{visibility:hidden}.bk-kanban.cdk-drop-list-dragging .bk-kanban-column:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@media (max-width: 640px){.bk-kanban-column{width:86vw}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: BkBadge, selector: "bk-badge", inputs: ["label", "variant", "color", "size", "dot", "removable", "customClass", "customBg", "customBorder", "customText"], outputs: ["clicked"] }, { kind: "component", type: BkButton, selector: "bk-button", inputs: ["variant", "size", "shadow", "label", "leftIcon", "rightIcon", "iconAlt", "type", "loading", "disabled", "buttonClass", "textClass", "spinnerClass"], outputs: ["clicked"] }, { kind: "component", type: BkIconButton, selector: "bk-icon-button", inputs: ["icon", "alt", "variant", "size", "disabled", "buttonClass"], outputs: ["clicked", "hovered"] }], encapsulation: i0.ViewEncapsulation.None });
14416
14597
  }
14417
14598
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BkKanban, decorators: [{
14418
14599
  type: Component,
14419
- args: [{ selector: 'bk-kanban', standalone: true, exportAs: 'bkKanban', imports: [NgTemplateOutlet, DragDropModule, BkBadge], encapsulation: ViewEncapsulation.None, host: {
14600
+ args: [{ selector: 'bk-kanban', standalone: true, exportAs: 'bkKanban', imports: [NgTemplateOutlet, DragDropModule, BkBadge, BkButton, BkIconButton], encapsulation: ViewEncapsulation.None, host: {
14420
14601
  class: 'bk-kanban-host'
14421
- }, template: "<div class=\"bk-kanban-board\" [class]=\"boardClass()\">\r\n <div\r\n class=\"bk-kanban\"\r\n [class.bk-kanban--scroll-wrapper]=\"columnScrollMode() === 'wrapper'\"\r\n cdkDropList\r\n cdkDropListOrientation=\"horizontal\"\r\n [cdkDropListData]=\"columns()\"\r\n [cdkDropListDisabled]=\"!columnDragEnabled()\"\r\n (cdkDropListDropped)=\"onColumnDrop($event)\"\r\n >\r\n @for (column of columns(); track column.id) {\r\n <div\r\n class=\"bk-kanban-column\"\r\n cdkDrag\r\n [cdkDragData]=\"column\"\r\n [cdkDragDisabled]=\"!columnDragEnabled()\"\r\n [class]=\"column.columnClass\"\r\n [class.bk-kanban-column-warn]=\"isColumnOverLimit(column)\"\r\n [class.bk-kanban-column-live-over]=\"isLiveOverLimit(column)\"\r\n >\r\n <div class=\"bk-kanban-column-header\" cdkDragHandle [class]=\"column.colHeaderClass\">\r\n <span class=\"bk-kanban-column-title\">{{ column.label }}</span>\r\n <bk-badge\r\n class=\"bk-kanban-column-count\"\r\n [label]=\"columnCountLabel(column)\"\r\n size=\"sm\"\r\n [color]=\"isColumnOverLimit(column) || isLiveOverLimit(column) ? 'Warning' : 'Gray'\"\r\n ></bk-badge>\r\n\r\n @if (columnHeaderActionsTemplate(); as actionsTpl) {\r\n <div class=\"bk-kanban-column-actions\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionsTpl\"\r\n [ngTemplateOutletContext]=\"columnActionsContext(column)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"bk-kanban-column-body\"\r\n cdkDropList\r\n [id]=\"column.id\"\r\n [cdkDropListData]=\"cardsFor(column.id)\"\r\n [cdkDropListConnectedTo]=\"connectedListIds(column.id)\"\r\n [cdkDropListSortingDisabled]=\"sortingDisabled()\"\r\n [cdkDropListDisabled]=\"!dragEnabled()\"\r\n (cdkDropListDropped)=\"onDrop($event, column)\"\r\n (cdkDropListEntered)=\"onListEntered($event, column)\"\r\n (cdkDropListExited)=\"onListExited(column)\"\r\n >\r\n @if (cardsFor(column.id).length === 0) {\r\n <div class=\"bk-kanban-empty\">\r\n @if (emptyStateTemplate(); as emptyTpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"emptyTpl\"\r\n [ngTemplateOutletContext]=\"emptyStateContext(column)\"\r\n ></ng-container>\r\n } @else {\r\n <span class=\"bk-kanban-empty-text\">{{ emptyStateText(column) }}</span>\r\n }\r\n </div>\r\n }\r\n\r\n @for (card of cardsFor(column.id); track resolveTrackBy(card, $index); let i = $index) {\r\n <div\r\n class=\"bk-kanban-card\"\r\n cdkDrag\r\n [cdkDragData]=\"card\"\r\n [cdkDragDisabled]=\"!dragEnabled()\"\r\n (cdkDragStarted)=\"onDragStarted(column)\"\r\n (cdkDragEnded)=\"onDragEnded()\"\r\n (click)=\"onCardClick($event, card)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardTemplate()\"\r\n [ngTemplateOutletContext]=\"cardContext(card, column, i)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".bk-kanban-host,.bk-kanban-board{display:block;height:100%}.bk-kanban{@apply flex items-stretch gap-4 w-full h-full overflow-x-auto;scroll-snap-type:x proximity}.bk-kanban--scroll-wrapper{align-items:flex-start;overflow-y:auto}.bk-kanban--scroll-wrapper .bk-kanban-column-body{overflow-y:visible}.bk-kanban-column{@apply flex flex-col shrink-0 w-[300px] max-w-[85vw] rounded-xl border border-[#EBEDF3] overflow-hidden;background:var(--bk-kanban-column-bg, #f9fafa);scroll-snap-align:start}.bk-kanban-column-header{@apply flex items-center justify-between gap-2 px-3 py-2.5;background:var(--bk-kanban-header-bg, #f9fafa);border-bottom:1px solid #ebedf3}.bk-kanban-column:not(.cdk-drag-disabled)>.bk-kanban-column-header{cursor:grab}.bk-kanban-column.cdk-drag-dragging>.bk-kanban-column-header{cursor:grabbing}.bk-kanban-column-title{@apply text-[13px] font-semibold text-[#15191E] truncate;}.bk-kanban-column-count{@apply shrink-0;}.bk-kanban-column-actions{@apply shrink-0 flex items-center;}.bk-kanban-column-body{@apply flex-1 flex flex-col gap-2 p-2.5 overflow-y-auto;min-height:96px}.bk-kanban-empty{@apply flex flex-1 flex-col items-center justify-center py-6 text-center;}.bk-kanban-empty-text{@apply text-xs font-medium text-[#78829D];}.bk-kanban-card{@apply bg-white rounded-lg border border-[#EBEDF3] p-3 cursor-pointer;box-shadow:0 1px 2px #1018280a;transition:box-shadow .15s ease,border-color .15s ease}.bk-kanban-card:hover{@apply border-[#C4CADA];box-shadow:0 2px 6px #10182814}.bk-kanban-card.cdk-drag-dragging{transition:none}.bk-kanban-column-warn{@apply border-amber-300 bg-amber-50;}.bk-kanban-column-warn .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-column-live-over{@apply border-amber-400 bg-amber-50;outline:2px dashed theme(\"colors.amber.400\");outline-offset:-2px}.bk-kanban-column-live-over .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-card.cdk-drag-preview{@apply rounded-lg;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;cursor:grabbing}.bk-kanban-card.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.6}.bk-kanban-column-body.cdk-drop-list-dragging .bk-kanban-card:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.bk-kanban-column.cdk-drag-preview{box-shadow:0 8px 10px -6px #0003,0 20px 25px -5px #00000024}.bk-kanban-column.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.5}.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-header,.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-body{visibility:hidden}.bk-kanban.cdk-drop-list-dragging .bk-kanban-column:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@media (max-width: 640px){.bk-kanban-column{width:86vw}}\n"] }]
14422
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: true }] }], cardTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardTemplate", required: true }] }], columnIdField: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnIdField", required: false }] }], dragEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragEnabled", required: false }] }], dragScope: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragScope", required: false }] }], wipLimitBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "wipLimitBehavior", required: false }] }], columnDragEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnDragEnabled", required: false }] }], columnScrollMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnScrollMode", required: false }] }], boardClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardClass", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], columnHeaderActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnHeaderActionsTemplate", required: false }] }], emptyStateTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyStateTemplate", required: false }] }], noDataText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataText", required: false }] }], dragPreviewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragPreviewMode", required: false }] }], cardMoved: [{ type: i0.Output, args: ["cardMoved"] }], cardClicked: [{ type: i0.Output, args: ["cardClicked"] }], columnLimitExceeded: [{ type: i0.Output, args: ["columnLimitExceeded"] }], columnMoved: [{ type: i0.Output, args: ["columnMoved"] }], columnActionTriggered: [{ type: i0.Output, args: ["columnActionTriggered"] }] } });
14602
+ }, template: "<div class=\"bk-kanban-board\" [class]=\"boardClass()\">\r\n <div\r\n class=\"bk-kanban\"\r\n [class.bk-kanban--scroll-wrapper]=\"columnScrollMode() === 'wrapper'\"\r\n cdkDropList\r\n cdkDropListOrientation=\"horizontal\"\r\n [cdkDropListData]=\"boardColumns()\"\r\n [cdkDropListDisabled]=\"!dragColumn()\"\r\n (cdkDropListDropped)=\"onColumnDrop($event)\"\r\n >\r\n @for (column of boardColumns(); track column.id) {\r\n <div\r\n class=\"bk-kanban-column\"\r\n cdkDrag\r\n [cdkDragData]=\"column\"\r\n [cdkDragDisabled]=\"!dragColumn()\"\r\n [class]=\"column.columnClass\"\r\n [class.bk-kanban-column-warn]=\"isColumnOverLimit(column)\"\r\n [class.bk-kanban-column-live-over]=\"isLiveOverLimit(column)\"\r\n >\r\n <div class=\"bk-kanban-column-header\" cdkDragHandle [class]=\"column.colHeaderClass\">\r\n <span class=\"bk-kanban-column-title\">{{ column.label }}</span>\r\n <bk-badge\r\n class=\"bk-kanban-column-count\"\r\n [label]=\"columnCountLabel(column)\"\r\n size=\"sm\"\r\n [color]=\"isColumnOverLimit(column) || isLiveOverLimit(column) ? 'Warning' : 'Gray'\"\r\n ></bk-badge>\r\n\r\n @if (columnHeaderActionsTemplate(); as actionsTpl) {\r\n <div class=\"bk-kanban-column-actions\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"actionsTpl\"\r\n [ngTemplateOutletContext]=\"columnActionsContext(column)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n @if (showAddButton(column)) {\r\n <bk-button\r\n class=\"bk-kanban-add-btn\"\r\n variant=\"primary\"\r\n size=\"xxsm\"\r\n [shadow]=\"false\"\r\n label=\"+\"\r\n [buttonClass]=\"'size-6 hover:bg-[#242424]'\"\r\n [attr.aria-label]=\"'Add item to ' + column.label\"\r\n (clicked)=\"onAddButtonClick(column)\"\r\n ></bk-button>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"bk-kanban-column-body\"\r\n cdkDropList\r\n [id]=\"column.id\"\r\n [cdkDropListData]=\"column.items\"\r\n [cdkDropListConnectedTo]=\"connectedListIds(column.id)\"\r\n [cdkDropListSortingDisabled]=\"sortingDisabled()\"\r\n [cdkDropListDisabled]=\"!dragActive()\"\r\n (cdkDropListDropped)=\"onItemDrop($event, column)\"\r\n (cdkDropListEntered)=\"onListEntered($event, column)\"\r\n (cdkDropListExited)=\"onListExited(column)\"\r\n >\r\n @if (column.items.length === 0) {\r\n <div class=\"bk-kanban-empty\">\r\n @if (emptyStateTemplate(); as emptyTpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"emptyTpl\"\r\n [ngTemplateOutletContext]=\"emptyStateContext(column)\"\r\n ></ng-container>\r\n } @else {\r\n <span class=\"bk-kanban-empty-text\">{{ emptyMessageFor(column) }}</span>\r\n }\r\n </div>\r\n }\r\n\r\n @for (card of column.items; track resolveTrackBy(card, $index); let i = $index) {\r\n <div\r\n class=\"bk-kanban-card\"\r\n cdkDrag\r\n [cdkDragData]=\"card\"\r\n [cdkDragDisabled]=\"!dragActive()\"\r\n (cdkDragStarted)=\"onDragStarted(column)\"\r\n (cdkDragEnded)=\"onDragEnded()\"\r\n (click)=\"onItemClick($event, card)\"\r\n >\r\n @if (showRemoveButton(column)) {\r\n <bk-icon-button\r\n class=\"bk-kanban-remove-btn\"\r\n [icon]=\"removeButtonIcon(card)\"\r\n [alt]=\"'Remove ' + card.label\"\r\n [variant]=\"removeButtonVariant(card)\"\r\n size=\"xxxsm\"\r\n (hovered)=\"onRemoveButtonHover(card, $event)\"\r\n (mousedown)=\"$event.stopPropagation()\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"$event.stopPropagation()\"\r\n (clicked)=\"onRemoveButtonClick(column, card)\"\r\n ></bk-icon-button>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardTemplate()\"\r\n [ngTemplateOutletContext]=\"cardContext(card, column, i)\"\r\n ></ng-container>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".bk-kanban-host{display:block;height:100%}:where(.bk-kanban-board){display:block;height:100%}.bk-kanban{@apply flex items-stretch gap-4 w-full h-full overflow-x-auto;scroll-snap-type:x proximity}.bk-kanban--scroll-wrapper{align-items:flex-start;overflow-y:auto}.bk-kanban--scroll-wrapper .bk-kanban-column-body{overflow-y:visible}:where(.bk-kanban-column){@apply flex flex-col shrink-0 w-[300px] max-w-[85vw] rounded-xl border border-[#EBEDF3] overflow-hidden;background:var(--bk-kanban-column-bg, #f9fafa);scroll-snap-align:start}:where(.bk-kanban-column-header){@apply flex items-center justify-between gap-2 px-3 py-2.5;background:var(--bk-kanban-header-bg, #f9fafa);border-bottom:1px solid #ebedf3}.bk-kanban-column:not(.cdk-drag-disabled)>.bk-kanban-column-header{cursor:grab}.bk-kanban-column.cdk-drag-dragging>.bk-kanban-column-header{cursor:grabbing}.bk-kanban-column-title{@apply text-[13px] font-semibold text-[#15191E] truncate;}.bk-kanban-column-count{@apply shrink-0;}.bk-kanban-column-actions{@apply shrink-0 flex items-center;}.bk-kanban-column-body{@apply flex-1 flex flex-col gap-2 p-2.5 overflow-y-auto;min-height:96px}.bk-kanban-empty{@apply flex flex-1 flex-col items-center justify-center py-6 text-center;}.bk-kanban-empty-text{@apply text-xs font-medium text-[#78829D];}.bk-kanban-add-btn{@apply shrink-0;}.bk-kanban-card{@apply relative bg-white rounded-lg border border-[#EBEDF3] p-3 cursor-pointer;box-shadow:0 1px 2px #1018280a;transition:box-shadow .15s ease,border-color .15s ease}.bk-kanban-remove-btn{@apply absolute top-1.5 right-1.5 z-10;}.bk-kanban-card:hover{@apply border-[#C4CADA];box-shadow:0 2px 6px #10182814}.bk-kanban-card.cdk-drag-dragging{transition:none}.bk-kanban-column-warn{@apply border-amber-300 bg-amber-50;}.bk-kanban-column-warn .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-column-live-over{@apply border-amber-400 bg-amber-50;outline:2px dashed theme(\"colors.amber.400\");outline-offset:-2px}.bk-kanban-column-live-over .bk-kanban-column-header{@apply bg-amber-50 border-amber-200;}.bk-kanban-card.cdk-drag-preview{@apply rounded-lg;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;cursor:grabbing}.bk-kanban-card.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.6}.bk-kanban-column-body.cdk-drop-list-dragging .bk-kanban-card:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.bk-kanban-column.cdk-drag-preview{box-shadow:0 8px 10px -6px #0003,0 20px 25px -5px #00000024}.bk-kanban-column.cdk-drag-placeholder{@apply border-dashed border-[#C4CADA] bg-[#F1F2F4];box-shadow:none;opacity:.5}.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-header,.bk-kanban-column.cdk-drag-placeholder .bk-kanban-column-body{visibility:hidden}.bk-kanban.cdk-drop-list-dragging .bk-kanban-column:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@media (max-width: 640px){.bk-kanban-column{width:86vw}}\n"] }]
14603
+ }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], cardTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardTemplate", required: true }] }], movementRestriction: [{ type: i0.Input, args: [{ isSignal: true, alias: "movementRestriction", required: false }] }], wipLimitBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "wipLimitBehavior", required: false }] }], dragColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragColumn", required: false }] }], columnReorderPayload: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnReorderPayload", required: false }] }], validateMove: [{ type: i0.Input, args: [{ isSignal: true, alias: "validateMove", required: false }] }], columnScrollMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnScrollMode", required: false }] }], boardClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardClass", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], columnHeaderActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnHeaderActionsTemplate", required: false }] }], emptyStateTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyStateTemplate", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], dragPreviewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragPreviewMode", required: false }] }], showAddItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddItem", required: false }] }], showRemoveItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRemoveItem", required: false }] }], columnsChanged: [{ type: i0.Output, args: ["columnsChanged"] }], columnLimitExceeded: [{ type: i0.Output, args: ["columnLimitExceeded"] }], itemClicked: [{ type: i0.Output, args: ["itemClicked"] }], itemMoveBetweenColumns: [{ type: i0.Output, args: ["itemMoveBetweenColumns"] }], columnActionTriggered: [{ type: i0.Output, args: ["columnActionTriggered"] }] } });
14423
14604
 
14424
14605
  /**
14425
14606
  * Marks an element inside a `cardTemplate` as its own click region.
@@ -14461,10 +14642,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
14461
14642
  /**
14462
14643
  * Types for bk-kanban.
14463
14644
  *
14464
- * The board is content-agnostic by design (see the component's own doc
14465
- * comment): it only ever moves opaque `T` values between columns and reports
14466
- * what happened. Every type here describes either a column, or one of the
14467
- * events the board reports never anything about what a card renders as.
14645
+ * Columns own their items a single nested tree is the source of truth:
14646
+ * `columns: KanbanColumn<T>[]`, each carrying its own `items: T[]`. There is
14647
+ * no separate flat cards array anywhere; grouping, ordering, and lookup all
14648
+ * fall out of the tree itself.
14649
+ *
14650
+ * Every drag, add, and remove operation emits only the column(s) actually
14651
+ * affected — never the whole board — each with its complete, current
14652
+ * `items` (column *reordering* is the one exception — see
14653
+ * `BkKanbanColumnReorderPayload`). There's deliberately no
14654
+ * `itemsMovedWithin` / `itemsMovedBetween` pair of "what changed" fields:
14655
+ * the consumer already gets the complete, ordered `items` for every affected
14656
+ * column, so diffing that against whatever it had before tells it exactly
14657
+ * what moved and where without the board also maintaining a second,
14658
+ * redundant description of the same fact.
14468
14659
  */
14469
14660
 
14470
14661
  /*