@asteby/metacore-runtime-react 20.0.0 → 20.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 20.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - eb6c65f: Kanban responsive board + cards truncate; sidebar nav lights the default view on a view-less landing
8
+
9
+ Two frontend UX fixes reported against the ops board.
10
+
11
+ **Kanban now adapts instead of overflowing (`@asteby/metacore-runtime-react`).**
12
+ `DynamicKanban` lanes were fixed-width (`w-72 shrink-0`), so with 4+ stages the
13
+ last lane was clipped off-viewport and long card text was cut by the card edge
14
+ with no ellipsis. Lanes are now responsive — `flex-1 min-w-[220px] max-w-[320px]`
15
+ — so they shrink to fit the available width and only scroll horizontally when
16
+ they genuinely can't fit. Card titles now `line-clamp-2 break-words` and the
17
+ secondary field rows carry `min-w-0` so long values ellipsize _inside_ the card
18
+ rather than being clipped by the border. The optimistic drag-to-move (PUT
19
+ `<base>/<id>`) is untouched.
20
+
21
+ **Sidebar nav active-state on the default/view-less landing (`@asteby/metacore-ui`,
22
+ `@asteby/metacore-starter-core`).** Landing on a model's bare list surface
23
+ (e.g. `/m/github_issues?per_page=15` — a transient param, no `view`) lit
24
+ _neither_ the "Tablero" (`?view=kanban`) nor the "Issues" (`?view=list`) nav
25
+ item, because the empty view bucket matched neither sibling's explicit view.
26
+ `checkIsActive` now treats the empty/`view=list`/`view=table` buckets as
27
+ "default-equivalent": a view-less current URL lights the list/default item while
28
+ the board (`?view=kanban`, never a default bucket) stays mutually exclusive. The
29
+ prior Board-vs-Issues exclusivity, `f_` filter and transient (page/sort/search)
30
+ behaviour are all preserved (18 matcher tests, +4 new). Ported to the
31
+ `starter-core` scaffold's embedded copy for parity.
32
+
33
+ - Updated dependencies [eb6c65f]
34
+ - @asteby/metacore-ui@2.6.1
35
+
36
+ ## 20.1.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 8de09a9: DynamicKanban: traduce el label de cada etapa via i18n (`t(stage.label)` con fallback al valor crudo) — antes mostraba la key cruda (ej. `integration_github.stage.backlog`) en vez de "Backlog". Y da min-height a las lanes para que el scroll horizontal del board quede abajo en vez de flotar cuando las columnas están vacías.
41
+
42
+ ### Patch Changes
43
+
44
+ - 8de09a9: fix(kanban): el drag-to-move ya no duplica `/me` en el PUT (causaba 404 "No se pudo mover la tarjeta"); el board respeta el ancho del padre (`min-w-0`) y deja de desbordarse horizontalmente.
45
+
3
46
  ## 20.0.0
4
47
 
5
48
  ### Minor Changes
@@ -44,8 +44,8 @@ export interface DynamicKanbanProps {
44
44
  /** Model key as registered on the backend (e.g. "issue"). */
45
45
  model: string;
46
46
  /**
47
- * Data endpoint base. Defaults to `/data/<model>`. The optimistic update
48
- * PUTs to `<base>/me/<id>`.
47
+ * Data endpoint base the org-scoped LIST endpoint (e.g.
48
+ * `/data/<model>/me`). The optimistic update PUTs to `<base>/<id>`.
49
49
  */
50
50
  endpoint?: string;
51
51
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAMhB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAejE;AAQD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,wBAAgB,YAAY,CACxB,OAAO,EAAE,GAAG,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAepB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,eAAe,EAAE,GAAG,SAAS,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACX,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAYpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,aAAa,EACvB,SAAS,SAAI,GACd;IAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkBhE;AA8BD,MAAM,WAAW,kBAAkB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAc,EACd,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,qBA+RpB"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAMhB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAejE;AAQD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,wBAAgB,YAAY,CACxB,OAAO,EAAE,GAAG,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAepB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,eAAe,EAAE,GAAG,SAAS,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACX,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAYpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,aAAa,EACvB,SAAS,SAAI,GACd;IAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkBhE;AA8BD,MAAM,WAAW,kBAAkB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAc,EACd,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,qBAmSpB"}
@@ -261,7 +261,11 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
261
261
  setRecords((rs) => rs.map((r) => String(r.id) === cardId ? { ...r, [groupByKey]: destStage } : r));
262
262
  try {
263
263
  const base = endpoint || `/data/${model}`;
264
- const res = (await api.put(`${base}/me/${cardId}`, {
264
+ // `base` is the org-scoped list endpoint (e.g. `/data/<model>/me`),
265
+ // so the per-record update is just `<base>/<id>` — same convention
266
+ // as DynamicTable/DynamicRelation. Appending an extra `/me` here
267
+ // produced `/data/<model>/me/me/<id>` → 404 on drag-to-move.
268
+ const res = (await api.put(`${base}/${cardId}`, {
265
269
  [groupByKey]: destStage,
266
270
  }));
267
271
  if (res?.data && res.data.success === false) {
@@ -280,7 +284,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
280
284
  }
281
285
  }, [api, endpoint, groupByKey, model, records, stageOfCard, t, transitions]);
282
286
  if (loading) {
283
- return (_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", children: [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "w-72 shrink-0 space-y-3", children: [_jsx(Skeleton, { className: "h-8 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" })] }, i))) }));
287
+ return (_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", children: [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "min-w-[220px] max-w-[320px] flex-1 space-y-3", children: [_jsx(Skeleton, { className: "h-8 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" })] }, i))) }));
284
288
  }
285
289
  if (!metadata || !groupByKey || stages.length === 0) {
286
290
  return (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground", children: t('kanban.noStages', {
@@ -298,7 +302,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
298
302
  order: Number.MAX_SAFE_INTEGER,
299
303
  });
300
304
  }
301
- return (_jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
305
+ return (_jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
302
306
  const cards = grouped.get(stage.key) ?? [];
303
307
  const droppableAllowed = !activeId ||
304
308
  stage.key === activeStage ||
@@ -318,7 +322,7 @@ function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
318
322
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
319
323
  isDark,
320
324
  });
321
- return (_jsxs("div", { ref: setNodeRef, className: "flex w-72 shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
325
+ return (_jsxs("div", { ref: setNodeRef, className: "flex min-w-[220px] max-w-[320px] flex-1 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
322
326
  opacity: dimmed ? 0.45 : 1,
323
327
  outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
324
328
  outlineOffset: 2,
@@ -329,14 +333,14 @@ function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, curr
329
333
  id: String(card.id),
330
334
  });
331
335
  const visibleActions = actions.filter((a) => isActionAllowedForRowState(a, card));
332
- return (_jsx(Card, { ref: setNodeRef, ...attributes, ...listeners, className: "cursor-grab active:cursor-grabbing border-border/70 shadow-sm", style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), visibleActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0 -mr-1 -mt-1",
336
+ return (_jsx(Card, { ref: setNodeRef, ...attributes, ...listeners, className: "cursor-grab active:cursor-grabbing border-border/70 shadow-sm", style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "line-clamp-2 min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), visibleActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0 -mr-1 -mt-1",
333
337
  // Don't start a drag / card click from the menu button.
334
338
  onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", onClick: (e) => e.stopPropagation(), children: visibleActions.map((a) => (_jsxs(DropdownMenuItem, { onClick: (e) => {
335
339
  e.stopPropagation();
336
340
  onAction(a, card);
337
- }, children: [_jsx(DynamicIcon, { name: a.icon || 'Zap', className: "mr-2 h-4 w-4" }), a.label] }, a.key))) })] }))] }), fieldCols.map((col) => (_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
341
+ }, children: [_jsx(DynamicIcon, { name: a.icon || 'Zap', className: "mr-2 h-4 w-4" }), a.label] }, a.key))) })] }))] }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
338
342
  }
339
343
  // Static preview rendered inside the DragOverlay (no dnd hooks, no menu).
340
344
  function CardPreview({ card, titleCol, fieldCols, locale, timeZone, currency, }) {
341
- return (_jsx(Card, { className: "w-72 cursor-grabbing border-primary/40 shadow-lg", children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsx("div", { className: "text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (String(card.id)) }), fieldCols.map((col) => (_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
345
+ return (_jsx(Card, { className: "w-72 cursor-grabbing border-primary/40 shadow-lg", children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsx("div", { className: "line-clamp-2 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (String(card.id)) }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
342
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "20.0.0",
3
+ "version": "20.1.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "sonner": ">=1.7",
39
39
  "zustand": ">=5",
40
40
  "@asteby/metacore-sdk": "^3.2.0",
41
- "@asteby/metacore-ui": "^2.6.0"
41
+ "@asteby/metacore-ui": "^2.6.1"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@tanstack/react-router": {
@@ -68,7 +68,7 @@
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
70
  "@asteby/metacore-sdk": "3.2.0",
71
- "@asteby/metacore-ui": "2.6.0"
71
+ "@asteby/metacore-ui": "2.6.1"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",
@@ -238,8 +238,8 @@ export interface DynamicKanbanProps {
238
238
  /** Model key as registered on the backend (e.g. "issue"). */
239
239
  model: string
240
240
  /**
241
- * Data endpoint base. Defaults to `/data/<model>`. The optimistic update
242
- * PUTs to `<base>/me/<id>`.
241
+ * Data endpoint base the org-scoped LIST endpoint (e.g.
242
+ * `/data/<model>/me`). The optimistic update PUTs to `<base>/<id>`.
243
243
  */
244
244
  endpoint?: string
245
245
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
@@ -411,7 +411,11 @@ export function DynamicKanban({
411
411
 
412
412
  try {
413
413
  const base = endpoint || `/data/${model}`
414
- const res = (await api.put(`${base}/me/${cardId}`, {
414
+ // `base` is the org-scoped list endpoint (e.g. `/data/<model>/me`),
415
+ // so the per-record update is just `<base>/<id>` — same convention
416
+ // as DynamicTable/DynamicRelation. Appending an extra `/me` here
417
+ // produced `/data/<model>/me/me/<id>` → 404 on drag-to-move.
418
+ const res = (await api.put(`${base}/${cardId}`, {
415
419
  [groupByKey]: destStage,
416
420
  })) as { data?: ApiResponse<any> }
417
421
  if (res?.data && res.data.success === false) {
@@ -437,7 +441,7 @@ export function DynamicKanban({
437
441
  return (
438
442
  <div className="flex gap-4 overflow-x-auto p-1">
439
443
  {[0, 1, 2, 3].map((i) => (
440
- <div key={i} className="w-72 shrink-0 space-y-3">
444
+ <div key={i} className="min-w-[220px] max-w-[320px] flex-1 space-y-3">
441
445
  <Skeleton className="h-8 w-full" />
442
446
  <Skeleton className="h-24 w-full" />
443
447
  <Skeleton className="h-24 w-full" />
@@ -473,7 +477,7 @@ export function DynamicKanban({
473
477
 
474
478
  return (
475
479
  <DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd}>
476
- <div className="flex gap-4 overflow-x-auto p-1" data-testid="kanban-board">
480
+ <div className="flex min-w-0 gap-4 overflow-x-auto p-1" data-testid="kanban-board">
477
481
  {lanes.map((stage) => {
478
482
  const cards = grouped.get(stage.key) ?? []
479
483
  const droppableAllowed =
@@ -576,7 +580,7 @@ function KanbanLane({ stage, count, isDark, dimmed, disabled, children }: Kanban
576
580
  return (
577
581
  <div
578
582
  ref={setNodeRef}
579
- className="flex w-72 shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity"
583
+ className="flex min-w-[220px] max-w-[320px] flex-1 flex-col rounded-lg border bg-muted/30 transition-opacity"
580
584
  style={{
581
585
  opacity: dimmed ? 0.45 : 1,
582
586
  outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
@@ -651,7 +655,7 @@ function KanbanCard({
651
655
  >
652
656
  <CardContent className="space-y-1.5 p-3">
653
657
  <div className="flex items-start justify-between gap-2">
654
- <div className="min-w-0 flex-1 text-sm font-medium leading-snug">
658
+ <div className="line-clamp-2 min-w-0 flex-1 break-words text-sm font-medium leading-snug">
655
659
  {titleCol ? (
656
660
  <ActivityValueRenderer
657
661
  value={card[titleCol.key]}
@@ -701,7 +705,7 @@ function KanbanCard({
701
705
  {fieldCols.map((col) => (
702
706
  <div
703
707
  key={col.key}
704
- className="flex items-center gap-1.5 text-xs text-muted-foreground"
708
+ className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"
705
709
  >
706
710
  <span className="shrink-0 opacity-70">{col.label}:</span>
707
711
  <span className="min-w-0 truncate">
@@ -732,7 +736,7 @@ function CardPreview({
732
736
  return (
733
737
  <Card className="w-72 cursor-grabbing border-primary/40 shadow-lg">
734
738
  <CardContent className="space-y-1.5 p-3">
735
- <div className="text-sm font-medium leading-snug">
739
+ <div className="line-clamp-2 break-words text-sm font-medium leading-snug">
736
740
  {titleCol ? (
737
741
  <ActivityValueRenderer
738
742
  value={card[titleCol.key]}
@@ -748,7 +752,7 @@ function CardPreview({
748
752
  {fieldCols.map((col) => (
749
753
  <div
750
754
  key={col.key}
751
- className="flex items-center gap-1.5 text-xs text-muted-foreground"
755
+ className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"
752
756
  >
753
757
  <span className="shrink-0 opacity-70">{col.label}:</span>
754
758
  <span className="min-w-0 truncate">