tembro 3.1.13 → 3.1.14

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.
@@ -89,16 +89,30 @@ export type KanbanBoardProps = Omit<React.ComponentProps<"div">, "defaultValue">
89
89
  overlayClassName?: string
90
90
  }
91
91
 
92
- type KanbanCardViewProps = {
93
- card: KanbanCard
94
- column: KanbanColumn
95
- index: number
96
- boardDisabled: boolean
92
+ type KanbanCardViewProps = {
93
+ card: KanbanCard
94
+ column: KanbanColumn
95
+ index: number
96
+ boardDisabled: boolean
97
97
  renderCard?: KanbanBoardProps["renderCard"]
98
- onCardClick?: KanbanBoardProps["onCardClick"]
99
- dragHandleLabel?: KanbanBoardProps["dragHandleLabel"]
100
- cardClassName?: string
101
- }
98
+ onCardClick?: KanbanBoardProps["onCardClick"]
99
+ dragHandleLabel?: KanbanBoardProps["dragHandleLabel"]
100
+ cardClassName?: string
101
+ onNativeDragStart?: (source: NativeDragSource) => void
102
+ onNativeDragEnd?: () => void
103
+ onNativeDrop?: (target: NativeDropTarget) => void
104
+ }
105
+
106
+ type NativeDragSource = {
107
+ cardKey: string
108
+ columnKey: string
109
+ index: number
110
+ }
111
+
112
+ type NativeDropTarget = {
113
+ columnKey: string
114
+ index?: number
115
+ }
102
116
 
103
117
  function createCardMap(columns: KanbanColumn[]) {
104
118
  return new Map(
@@ -108,10 +122,10 @@ function createCardMap(columns: KanbanColumn[]) {
108
122
  )
109
123
  }
110
124
 
111
- function moveKanbanCards(
112
- columns: KanbanColumn[],
113
- event: DragOverEvent | DragEndEvent
114
- ) {
125
+ function moveKanbanCards(
126
+ columns: KanbanColumn[],
127
+ event: DragOverEvent | DragEndEvent
128
+ ) {
115
129
  const cardMap = createCardMap(columns)
116
130
  const cardIdsByColumn = Object.fromEntries(
117
131
  columns.map((column) => [
@@ -126,8 +140,57 @@ function moveKanbanCards(
126
140
  cards: (movedIds[getColumnId(column.key)] ?? [])
127
141
  .map((id) => cardMap.get(String(id)))
128
142
  .filter((card): card is KanbanCard => Boolean(card)),
129
- }))
130
- }
143
+ }))
144
+ }
145
+
146
+ function moveKanbanCardToTarget(
147
+ columns: KanbanColumn[],
148
+ source: NativeDragSource,
149
+ target: NativeDropTarget
150
+ ): { columns: KanbanColumn[]; change: KanbanCardMove } | null {
151
+ const sourceColumn = columns.find((column) => column.key === source.columnKey)
152
+ const targetColumn = columns.find((column) => column.key === target.columnKey)
153
+ if (!sourceColumn || !targetColumn) return null
154
+
155
+ const card = sourceColumn.cards.find((item) => item.key === source.cardKey)
156
+ if (!card) return null
157
+
158
+ const sourceIndex = sourceColumn.cards.findIndex((item) => item.key === source.cardKey)
159
+ if (sourceIndex < 0) return null
160
+
161
+ const targetCards = targetColumn.cards
162
+ let targetIndex = target.index ?? targetCards.length
163
+ if (sourceColumn.key === targetColumn.key && targetIndex > sourceIndex) {
164
+ targetIndex -= 1
165
+ }
166
+ targetIndex = Math.max(0, Math.min(targetIndex, targetCards.length))
167
+ if (sourceColumn.key === targetColumn.key && sourceIndex === targetIndex) return null
168
+
169
+ const nextColumns = columns.map((column) => {
170
+ if (column.key === sourceColumn.key) {
171
+ return { ...column, cards: column.cards.filter((item) => item.key !== source.cardKey) }
172
+ }
173
+ return column
174
+ }).map((column) => {
175
+ if (column.key !== targetColumn.key) return column
176
+ const nextCards = [...column.cards]
177
+ nextCards.splice(targetIndex, 0, card)
178
+ return { ...column, cards: nextCards }
179
+ })
180
+
181
+ const resolvedTargetColumn = nextColumns.find((column) => column.key === targetColumn.key) ?? targetColumn
182
+
183
+ return {
184
+ columns: nextColumns,
185
+ change: {
186
+ card,
187
+ fromColumn: sourceColumn,
188
+ toColumn: resolvedTargetColumn,
189
+ fromIndex: sourceIndex,
190
+ toIndex: targetIndex,
191
+ },
192
+ }
193
+ }
131
194
 
132
195
  const KanbanDragHandle = React.forwardRef<HTMLButtonElement, {
133
196
  label: string
@@ -169,16 +232,19 @@ function DefaultKanbanCard({ card, handle }: { card: KanbanCard; handle: React.R
169
232
  )
170
233
  }
171
234
 
172
- function KanbanCardView({
173
- card,
174
- column,
175
- index,
235
+ function KanbanCardView({
236
+ card,
237
+ column,
238
+ index,
176
239
  boardDisabled,
177
240
  renderCard,
178
- onCardClick,
179
- dragHandleLabel,
180
- cardClassName,
181
- }: KanbanCardViewProps) {
241
+ onCardClick,
242
+ dragHandleLabel,
243
+ cardClassName,
244
+ onNativeDragStart,
245
+ onNativeDragEnd,
246
+ onNativeDrop,
247
+ }: KanbanCardViewProps) {
182
248
  const disabled = boardDisabled || column.disabled === true || card.disabled === true
183
249
  const { ref, handleRef, isDragging, isDropTarget } = useSortable({
184
250
  id: getCardId(card.key),
@@ -206,16 +272,17 @@ function KanbanCardView({
206
272
  }
207
273
 
208
274
  return (
209
- <article
210
- ref={ref as React.Ref<HTMLElement>}
211
- data-slot="kanban-card"
275
+ <article
276
+ ref={ref as React.Ref<HTMLElement>}
277
+ data-slot="kanban-card"
212
278
  data-dragging={isDragging || undefined}
213
279
  data-drop-target={isDropTarget || undefined}
214
280
  data-disabled={disabled || undefined}
215
281
  role={onCardClick && !disabled ? "button" : undefined}
216
282
  tabIndex={onCardClick && !disabled ? 0 : undefined}
217
- aria-label={typeof card.title === "string" ? card.title : undefined}
218
- className={cn(
283
+ aria-label={typeof card.title === "string" ? card.title : undefined}
284
+ draggable={!disabled}
285
+ className={cn(
219
286
  "rounded-lg outline-none transition-[opacity,box-shadow]",
220
287
  onCardClick && !disabled && "cursor-pointer focus-visible:ring-2 focus-visible:ring-ring",
221
288
  isDropTarget && "ring-2 ring-primary/30",
@@ -223,10 +290,27 @@ function KanbanCardView({
223
290
  disabled && "opacity-55",
224
291
  cardClassName
225
292
  )}
226
- onClick={() => {
227
- if (!disabled) onCardClick?.(card, column)
228
- }}
229
- onKeyDown={(event) => {
293
+ onClick={() => {
294
+ if (!disabled) onCardClick?.(card, column)
295
+ }}
296
+ onDragStart={(event) => {
297
+ if (disabled) return
298
+ const source = { cardKey: card.key, columnKey: column.key, index }
299
+ event.dataTransfer.effectAllowed = "move"
300
+ event.dataTransfer.setData("text/plain", JSON.stringify(source))
301
+ onNativeDragStart?.(source)
302
+ }}
303
+ onDragEnd={onNativeDragEnd}
304
+ onDragOver={(event) => {
305
+ if (!disabled) event.preventDefault()
306
+ }}
307
+ onDrop={(event) => {
308
+ if (disabled) return
309
+ event.preventDefault()
310
+ event.stopPropagation()
311
+ onNativeDrop?.({ columnKey: column.key, index })
312
+ }}
313
+ onKeyDown={(event) => {
230
314
  if (event.target !== event.currentTarget || disabled || !onCardClick) return
231
315
  if (event.key === "Enter" || event.key === " ") {
232
316
  event.preventDefault()
@@ -246,18 +330,24 @@ function KanbanColumnView({
246
330
  onCardClick,
247
331
  dragHandleLabel,
248
332
  emptyColumn,
249
- columnClassName,
250
- cardClassName,
251
- }: {
333
+ columnClassName,
334
+ cardClassName,
335
+ onNativeDragStart,
336
+ onNativeDragEnd,
337
+ onNativeDrop,
338
+ }: {
252
339
  column: KanbanColumn
253
340
  boardDisabled: boolean
254
341
  renderCard?: KanbanBoardProps["renderCard"]
255
342
  onCardClick?: KanbanBoardProps["onCardClick"]
256
343
  dragHandleLabel?: KanbanBoardProps["dragHandleLabel"]
257
- emptyColumn: React.ReactNode
258
- columnClassName?: string
259
- cardClassName?: string
260
- }) {
344
+ emptyColumn: React.ReactNode
345
+ columnClassName?: string
346
+ cardClassName?: string
347
+ onNativeDragStart?: (source: NativeDragSource) => void
348
+ onNativeDragEnd?: () => void
349
+ onNativeDrop?: (target: NativeDropTarget) => void
350
+ }) {
261
351
  const { ref, isDropTarget } = useDroppable({
262
352
  id: getColumnId(column.key),
263
353
  type: "tembro-kanban-column",
@@ -274,13 +364,21 @@ function KanbanColumnView({
274
364
  data-drop-target={isDropTarget || undefined}
275
365
  data-disabled={column.disabled || undefined}
276
366
  aria-label={typeof column.title === "string" ? column.title : undefined}
277
- className={cn(
367
+ className={cn(
278
368
  "grid min-h-40 content-start gap-3 rounded-lg border bg-muted/35 p-3 transition-[border-color,background-color,box-shadow]",
279
369
  isDropTarget && "border-primary/60 bg-primary/5 ring-2 ring-primary/15",
280
370
  column.disabled && "opacity-60",
281
- columnClassName
282
- )}
283
- >
371
+ columnClassName
372
+ )}
373
+ onDragOver={(event) => {
374
+ if (!(boardDisabled || column.disabled === true)) event.preventDefault()
375
+ }}
376
+ onDrop={(event) => {
377
+ if (boardDisabled || column.disabled === true) return
378
+ event.preventDefault()
379
+ onNativeDrop?.({ columnKey: column.key, index: column.cards.length })
380
+ }}
381
+ >
284
382
  <div className="flex items-start justify-between gap-3">
285
383
  <div className="grid gap-0.5">
286
384
  <h3 className="text-sm font-semibold text-foreground">{column.title}</h3>
@@ -306,9 +404,12 @@ function KanbanColumnView({
306
404
  boardDisabled={boardDisabled}
307
405
  renderCard={renderCard}
308
406
  onCardClick={onCardClick}
309
- dragHandleLabel={dragHandleLabel}
310
- cardClassName={cardClassName}
311
- />
407
+ dragHandleLabel={dragHandleLabel}
408
+ cardClassName={cardClassName}
409
+ onNativeDragStart={onNativeDragStart}
410
+ onNativeDragEnd={onNativeDragEnd}
411
+ onNativeDrop={onNativeDrop}
412
+ />
312
413
  ))
313
414
  )}
314
415
  </div>
@@ -334,9 +435,10 @@ function KanbanBoard({
334
435
  ...props
335
436
  }: KanbanBoardProps) {
336
437
  const [internalColumns, setInternalColumns] = React.useState<KanbanColumn[]>(() => defaultColumns)
337
- const [draftColumns, setDraftColumns] = React.useState<KanbanColumn[] | null>(null)
338
- const [activeCardId, setActiveCardId] = React.useState<string | null>(null)
339
- const draftColumnsRef = React.useRef<KanbanColumn[] | null>(null)
438
+ const [draftColumns, setDraftColumns] = React.useState<KanbanColumn[] | null>(null)
439
+ const [activeCardId, setActiveCardId] = React.useState<string | null>(null)
440
+ const draftColumnsRef = React.useRef<KanbanColumn[] | null>(null)
441
+ const nativeDragRef = React.useRef<NativeDragSource | null>(null)
340
442
  const columns = columnsProp ?? internalColumns
341
443
  const renderedColumns = draftColumns ?? columns
342
444
  const isControlled = columnsProp !== undefined
@@ -361,7 +463,7 @@ function KanbanBoard({
361
463
  setDraftColumns(nextColumns)
362
464
  }
363
465
 
364
- function handleDragEnd(event: DragEndEvent) {
466
+ function handleDragEnd(event: DragEndEvent) {
365
467
  const source = event.operation.source
366
468
  setActiveCardId(null)
367
469
 
@@ -393,8 +495,21 @@ function KanbanBoard({
393
495
  }
394
496
  if (!isControlled) setInternalColumns(finalColumns)
395
497
  onColumnsChange?.(finalColumns, change)
396
- onCardMove?.(change)
397
- }
498
+ onCardMove?.(change)
499
+ }
500
+
501
+ function handleNativeDrop(target: NativeDropTarget) {
502
+ const source = nativeDragRef.current
503
+ nativeDragRef.current = null
504
+ if (!source) return
505
+
506
+ const result = moveKanbanCardToTarget(columns, source, target)
507
+ if (!result) return
508
+
509
+ if (!isControlled) setInternalColumns(result.columns)
510
+ onColumnsChange?.(result.columns, result.change)
511
+ onCardMove?.(result.change)
512
+ }
398
513
 
399
514
  const activeEntry = activeCardId
400
515
  ? renderedColumns
@@ -425,11 +540,14 @@ function KanbanBoard({
425
540
  boardDisabled={disabled}
426
541
  renderCard={renderCard}
427
542
  onCardClick={onCardClick}
428
- dragHandleLabel={dragHandleLabel}
429
- emptyColumn={emptyColumn}
430
- columnClassName={columnClassName}
431
- cardClassName={cardClassName}
432
- />
543
+ dragHandleLabel={dragHandleLabel}
544
+ emptyColumn={emptyColumn}
545
+ columnClassName={columnClassName}
546
+ cardClassName={cardClassName}
547
+ onNativeDragStart={(source) => { nativeDragRef.current = source }}
548
+ onNativeDragEnd={() => { nativeDragRef.current = null }}
549
+ onNativeDrop={handleNativeDrop}
550
+ />
433
551
  ))}
434
552
  </div>
435
553
 
@@ -1,4 +1,4 @@
1
- export const PACKAGE_LATEST_VERSION = "3.1.13"
1
+ export const PACKAGE_LATEST_VERSION = "3.1.14"
2
2
  export const PACKAGE_LATEST_RELEASE_DATE = "July 15, 2026"
3
3
  export const PACKAGE_DEPENDENCY_RANGE = PACKAGE_LATEST_VERSION
4
4
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://tembro.dev/registry.schema.json",
3
3
  "name": "tembro",
4
- "version": "3.1.13",
4
+ "version": "3.1.14",
5
5
  "description": "Reusable React + TypeScript UI kit registry",
6
6
  "groups": {
7
7
  "ui": [
@@ -25,7 +25,7 @@ export function HeroSection() {
25
25
  <div className="flex flex-wrap gap-2">
26
26
  <Button leftIcon={<PlusIcon className="size-4" />}>Create</Button>
27
27
  <Button variant="outline" leftIcon={<SettingsIcon className="size-4" />}>Settings</Button>
28
- <CopyButton value="npx tembro@3.1.13 list --json">Copy list command</CopyButton>
28
+ <CopyButton value="npx tembro@3.1.14 list --json">Copy list command</CopyButton>
29
29
  </div>
30
30
  </div>
31
31
 
@@ -33,7 +33,7 @@ export function HeroSection() {
33
33
  <StatisticCard label="Registry components" value={moduleCount} change="visible below" trend="up" description="tembro add <name>" />
34
34
  <StatisticCard label="Local source files" value="152" change="with hooks/lib" trend="up" description="installed by CLI" />
35
35
  <StatisticCard label="Categories" value={registryGroups.length} change="all shown" trend="up" description="actions to wizard" />
36
- <StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@3.1.13" />
36
+ <StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@3.1.14" />
37
37
  </StatisticGrid>
38
38
  </div>
39
39
  </section>
@@ -72,7 +72,7 @@ export function WorkbenchSidebar({ selectedKey, onSelect }: WorkbenchSidebarProp
72
72
  }),
73
73
  ]}
74
74
  footerAccount={{
75
- label: "tembro@3.1.13",
75
+ label: "tembro@3.1.14",
76
76
  description: `${moduleCount} registry components`,
77
77
  }}
78
78
  activeIndicator="bar"
package/registry.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://tembro.dev/registry.schema.json",
3
3
  "name": "tembro",
4
- "version": "3.1.13",
4
+ "version": "3.1.14",
5
5
  "description": "Reusable React + TypeScript UI kit registry",
6
6
  "groups": {
7
7
  "ui": [