@schandlergarcia/sf-web-components 2.3.17 → 2.5.0
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/.a4drules/skills/command-center-builder/SKILL.md +3 -2
- package/.a4drules/skills/component-library/SKILL.md +50 -4
- package/.a4drules/skills/component-library/card-components.md +88 -0
- package/.a4drules/skills/component-library/when-to-use.md +1 -0
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +12 -13
- package/README.md +0 -15
- package/dist/components/library/cards/KanbanBoard.js +313 -0
- package/dist/components/library/cards/KanbanBoard.js.map +1 -0
- package/dist/components/library/index.js +60 -57
- package/dist/components/library/index.js.map +1 -1
- package/dist/components/workspace/ComponentRegistry.js +5 -2
- package/dist/components/workspace/ComponentRegistry.js.map +1 -1
- package/dist/index.js +84 -82
- package/dist/index.js.map +1 -1
- package/dist/styles/global.css +44 -57
- package/package.json +7 -2
- package/scripts/apply-brand.mjs +47 -30
- package/scripts/postinstall.mjs +1 -11
- package/src/components/library/cards/KanbanBoard.jsx +507 -0
- package/src/components/library/index.jsx +1 -0
- package/src/styles/global.css +44 -57
- package/brands/engine/PARTNER_HUB_PRD.md +0 -584
- package/brands/engine/agentApiConfig.ts +0 -36
- package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
- package/brands/engine/app/api/graphqlClient.ts +0 -25
- package/brands/engine/app/api/partnerQueries.ts +0 -212
- package/brands/engine/app/appLayout.tsx +0 -5
- package/brands/engine/app/components/AgentPanel.tsx +0 -541
- package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
- package/brands/engine/app/components/Data360Widget.tsx +0 -301
- package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
- package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
- package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
- package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
- package/brands/engine/app/config/agentApi.ts +0 -36
- package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
- package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
- package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
- package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
- package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
- package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
- package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
- package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
- package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
- package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
- package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
- package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
- package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
- package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
- package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
- package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
- package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
- package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
- package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
- package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
- package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
- package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
- package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
- package/brands/engine/app/navigationMenu.tsx +0 -80
- package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
- package/brands/engine/app/pages/AccountSearch.tsx +0 -305
- package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
- package/brands/engine/app/pages/DataTest.tsx +0 -78
- package/brands/engine/app/pages/Home.tsx +0 -5
- package/brands/engine/app/pages/NotFound.tsx +0 -19
- package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
- package/brands/engine/app/pages/Search.tsx +0 -13
- package/brands/engine/app/router-utils.tsx +0 -35
- package/brands/engine/app/routes.tsx +0 -39
- package/brands/engine/app/styles/global.css +0 -269
- package/brands/engine/brand.css +0 -40
- package/brands/engine/engine-command-center-prd.md +0 -575
- package/brands/engine/engine-live-data.js +0 -135
- package/brands/engine/engine-sample-data.js +0 -378
- package/brands/engine/engine_logo.png +0 -0
- package/brands/engine/global.css +0 -269
- package/brands/engine/partner-hub-sample-data.js +0 -281
- package/brands/engine/schema.graphql +0 -292
- package/brands/engine/useEngineLiveData.ts +0 -49
- package/brands/engine/useEvaAgent.ts +0 -288
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import React, { useMemo, useState, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DndContext,
|
|
4
|
+
DragOverlay,
|
|
5
|
+
PointerSensor,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
closestCenter,
|
|
10
|
+
useDroppable,
|
|
11
|
+
} from "@dnd-kit/core";
|
|
12
|
+
import {
|
|
13
|
+
SortableContext,
|
|
14
|
+
useSortable,
|
|
15
|
+
verticalListSortingStrategy,
|
|
16
|
+
sortableKeyboardCoordinates,
|
|
17
|
+
arrayMove,
|
|
18
|
+
} from "@dnd-kit/sortable";
|
|
19
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
20
|
+
import BaseCard from "./BaseCard";
|
|
21
|
+
import UIText from "../ui/Text";
|
|
22
|
+
import UIChip from "../ui/Chip";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* KanbanBoard
|
|
26
|
+
*
|
|
27
|
+
* A swim-lane / board view with structured cards and (optional) drag-and-drop
|
|
28
|
+
* powered by @dnd-kit. Designed to live inside a CommandCenter dashboard and
|
|
29
|
+
* to behave consistently with ListCard / TableCard.
|
|
30
|
+
*
|
|
31
|
+
* Data shapes:
|
|
32
|
+
* columns: [{ id, title, accent?, limit?, footer?, emptyMessage? }]
|
|
33
|
+
* cards: [{ id, columnId, title, subtitle?, description?, badges?,
|
|
34
|
+
* status?, avatar?, meta?, actions?, accent? }]
|
|
35
|
+
*
|
|
36
|
+
* Callbacks:
|
|
37
|
+
* onCardClick(card)
|
|
38
|
+
* onCardMove({ cardId, fromColumnId, toColumnId, fromIndex, toIndex })
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const ACCENT_RING = {
|
|
42
|
+
default: "ring-slate-200 dark:ring-slate-800",
|
|
43
|
+
primary: "ring-brand-500/40",
|
|
44
|
+
success: "ring-emerald-500/40",
|
|
45
|
+
warning: "ring-amber-500/40",
|
|
46
|
+
danger: "ring-rose-500/40",
|
|
47
|
+
info: "ring-sky-500/40",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ACCENT_DOT = {
|
|
51
|
+
default: "bg-slate-400",
|
|
52
|
+
primary: "bg-brand-500",
|
|
53
|
+
success: "bg-emerald-500",
|
|
54
|
+
warning: "bg-amber-500",
|
|
55
|
+
danger: "bg-rose-500",
|
|
56
|
+
info: "bg-sky-500",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ACCENT_BORDER_LEFT = {
|
|
60
|
+
default: "border-l-slate-300 dark:border-l-slate-700",
|
|
61
|
+
primary: "border-l-brand-500",
|
|
62
|
+
success: "border-l-emerald-500",
|
|
63
|
+
warning: "border-l-amber-500",
|
|
64
|
+
danger: "border-l-rose-500",
|
|
65
|
+
info: "border-l-sky-500",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function CardBadges({ badges }) {
|
|
69
|
+
if (!badges || badges.length === 0) return null;
|
|
70
|
+
return (
|
|
71
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
72
|
+
{badges.map((b, i) => {
|
|
73
|
+
if (React.isValidElement(b)) return <span key={i}>{b}</span>;
|
|
74
|
+
if (typeof b === "string") {
|
|
75
|
+
return (
|
|
76
|
+
<UIChip key={i} size="sm" variant="default">
|
|
77
|
+
{b}
|
|
78
|
+
</UIChip>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<UIChip
|
|
83
|
+
key={b.id ?? i}
|
|
84
|
+
size={b.size ?? "sm"}
|
|
85
|
+
variant={b.variant ?? "default"}
|
|
86
|
+
color={b.color}
|
|
87
|
+
>
|
|
88
|
+
{b.label ?? b.text}
|
|
89
|
+
</UIChip>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CardAvatar({ avatar, title }) {
|
|
97
|
+
if (!avatar) return null;
|
|
98
|
+
if (typeof avatar === "string") {
|
|
99
|
+
return (
|
|
100
|
+
<img
|
|
101
|
+
src={avatar}
|
|
102
|
+
alt=""
|
|
103
|
+
className="h-7 w-7 shrink-0 rounded-full border border-slate-200 object-cover dark:border-slate-800"
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (React.isValidElement(avatar)) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="grid h-7 w-7 shrink-0 place-items-center rounded-full border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-800">
|
|
110
|
+
{avatar}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
<div className="grid h-7 w-7 shrink-0 place-items-center rounded-full border border-slate-200 bg-slate-100 text-[10px] font-semibold text-slate-700 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-200">
|
|
116
|
+
{String(title ?? "??")
|
|
117
|
+
.slice(0, 2)
|
|
118
|
+
.toUpperCase()}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function KanbanCardBody({ card, accent, dragging, onClick, dense }) {
|
|
124
|
+
const accentBorder =
|
|
125
|
+
ACCENT_BORDER_LEFT[card?.accent ?? accent ?? "default"] ?? ACCENT_BORDER_LEFT.default;
|
|
126
|
+
|
|
127
|
+
const Comp = onClick ? "button" : "div";
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Comp
|
|
131
|
+
type={onClick ? "button" : undefined}
|
|
132
|
+
onClick={onClick ? () => onClick(card) : undefined}
|
|
133
|
+
className={[
|
|
134
|
+
"group block w-full rounded-xl border bg-white text-left shadow-sm transition",
|
|
135
|
+
"border-slate-200 dark:border-slate-800 dark:bg-slate-900",
|
|
136
|
+
"border-l-4",
|
|
137
|
+
accentBorder,
|
|
138
|
+
dense ? "p-2.5" : "p-3",
|
|
139
|
+
dragging ? "opacity-60 ring-2 ring-brand-500" : "hover:-translate-y-[1px] hover:shadow-md",
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(" ")}
|
|
143
|
+
>
|
|
144
|
+
<div className="flex items-start gap-2">
|
|
145
|
+
<CardAvatar avatar={card.avatar} title={card.title} />
|
|
146
|
+
<div className="min-w-0 flex-1">
|
|
147
|
+
<UIText as="div" size="sm" weight="medium" className="truncate">
|
|
148
|
+
{card.title}
|
|
149
|
+
</UIText>
|
|
150
|
+
{card.subtitle ? (
|
|
151
|
+
<UIText as="div" size="xs" muted className="mt-0.5 truncate">
|
|
152
|
+
{card.subtitle}
|
|
153
|
+
</UIText>
|
|
154
|
+
) : null}
|
|
155
|
+
</div>
|
|
156
|
+
{card.status ? (
|
|
157
|
+
<span
|
|
158
|
+
aria-label={String(card.status)}
|
|
159
|
+
className={[
|
|
160
|
+
"mt-1.5 h-2 w-2 shrink-0 rounded-full",
|
|
161
|
+
ACCENT_DOT[card.status] ?? ACCENT_DOT.default,
|
|
162
|
+
].join(" ")}
|
|
163
|
+
/>
|
|
164
|
+
) : null}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{card.description ? (
|
|
168
|
+
<UIText as="div" size="xs" muted className="mt-2 line-clamp-3 text-left">
|
|
169
|
+
{card.description}
|
|
170
|
+
</UIText>
|
|
171
|
+
) : null}
|
|
172
|
+
|
|
173
|
+
<CardBadges badges={card.badges} />
|
|
174
|
+
|
|
175
|
+
{card.meta && card.meta.length > 0 ? (
|
|
176
|
+
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-slate-500 dark:text-slate-400">
|
|
177
|
+
{card.meta.map((m, i) => {
|
|
178
|
+
if (React.isValidElement(m)) return <span key={i}>{m}</span>;
|
|
179
|
+
if (typeof m === "string") return <span key={i}>{m}</span>;
|
|
180
|
+
return (
|
|
181
|
+
<span key={m.id ?? i} className="inline-flex items-center gap-1">
|
|
182
|
+
{m.icon ? <span className="opacity-70">{m.icon}</span> : null}
|
|
183
|
+
<span>{m.label ?? m.text}</span>
|
|
184
|
+
</span>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
|
|
190
|
+
{card.actions ? (
|
|
191
|
+
<div
|
|
192
|
+
className="mt-2 flex justify-end gap-1.5"
|
|
193
|
+
onClick={(e) => e.stopPropagation()}
|
|
194
|
+
>
|
|
195
|
+
{card.actions}
|
|
196
|
+
</div>
|
|
197
|
+
) : null}
|
|
198
|
+
</Comp>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function SortableKanbanCard({ card, accent, onClick, isDraggable, dense }) {
|
|
203
|
+
const sortable = useSortable({
|
|
204
|
+
id: card.id,
|
|
205
|
+
data: { columnId: card.columnId, type: "card" },
|
|
206
|
+
disabled: !isDraggable,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const style = {
|
|
210
|
+
transform: CSS.Translate.toString(sortable.transform),
|
|
211
|
+
transition: sortable.transition,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
ref={sortable.setNodeRef}
|
|
217
|
+
style={style}
|
|
218
|
+
{...(isDraggable ? sortable.attributes : {})}
|
|
219
|
+
{...(isDraggable ? sortable.listeners : {})}
|
|
220
|
+
className={isDraggable ? "touch-none" : undefined}
|
|
221
|
+
>
|
|
222
|
+
<KanbanCardBody
|
|
223
|
+
card={card}
|
|
224
|
+
accent={accent}
|
|
225
|
+
dragging={sortable.isDragging}
|
|
226
|
+
onClick={onClick}
|
|
227
|
+
dense={dense}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function KanbanColumn({
|
|
234
|
+
column,
|
|
235
|
+
cards,
|
|
236
|
+
accent,
|
|
237
|
+
onCardClick,
|
|
238
|
+
isDraggable,
|
|
239
|
+
dense,
|
|
240
|
+
showCounts,
|
|
241
|
+
showLimits,
|
|
242
|
+
emptyMessage,
|
|
243
|
+
}) {
|
|
244
|
+
const droppable = useDroppable({
|
|
245
|
+
id: `column:${column.id}`,
|
|
246
|
+
data: { columnId: column.id, type: "column" },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const count = cards.length;
|
|
250
|
+
const overLimit = column.limit != null && count > column.limit;
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div className="flex w-72 shrink-0 flex-col rounded-2xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900/60">
|
|
254
|
+
<div className="flex items-center justify-between gap-2 border-b border-slate-200 px-3 py-2 dark:border-slate-800">
|
|
255
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
256
|
+
<span
|
|
257
|
+
aria-hidden
|
|
258
|
+
className={[
|
|
259
|
+
"h-2 w-2 shrink-0 rounded-full",
|
|
260
|
+
ACCENT_DOT[column.accent ?? accent ?? "default"] ?? ACCENT_DOT.default,
|
|
261
|
+
].join(" ")}
|
|
262
|
+
/>
|
|
263
|
+
<UIText as="div" size="sm" weight="medium" className="truncate">
|
|
264
|
+
{column.title}
|
|
265
|
+
</UIText>
|
|
266
|
+
{showCounts ? (
|
|
267
|
+
<UIChip
|
|
268
|
+
size="sm"
|
|
269
|
+
variant="default"
|
|
270
|
+
className={overLimit ? "ring-1 ring-rose-500" : undefined}
|
|
271
|
+
>
|
|
272
|
+
{showLimits && column.limit != null ? `${count} / ${column.limit}` : count}
|
|
273
|
+
</UIChip>
|
|
274
|
+
) : null}
|
|
275
|
+
</div>
|
|
276
|
+
{column.actions ? <div className="shrink-0">{column.actions}</div> : null}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div
|
|
280
|
+
ref={droppable.setNodeRef}
|
|
281
|
+
className={[
|
|
282
|
+
"flex flex-1 flex-col gap-2 overflow-y-auto p-2",
|
|
283
|
+
droppable.isOver ? "bg-brand-50/50 dark:bg-brand-950/30" : "",
|
|
284
|
+
]
|
|
285
|
+
.filter(Boolean)
|
|
286
|
+
.join(" ")}
|
|
287
|
+
style={{ minHeight: 80 }}
|
|
288
|
+
>
|
|
289
|
+
<SortableContext
|
|
290
|
+
items={cards.map((c) => c.id)}
|
|
291
|
+
strategy={verticalListSortingStrategy}
|
|
292
|
+
>
|
|
293
|
+
{cards.length === 0 ? (
|
|
294
|
+
<div className="grid flex-1 place-items-center px-2 py-6">
|
|
295
|
+
<UIText as="div" size="xs" muted className="text-center">
|
|
296
|
+
{column.emptyMessage ?? emptyMessage ?? "No items"}
|
|
297
|
+
</UIText>
|
|
298
|
+
</div>
|
|
299
|
+
) : (
|
|
300
|
+
cards.map((card) => (
|
|
301
|
+
<SortableKanbanCard
|
|
302
|
+
key={card.id}
|
|
303
|
+
card={card}
|
|
304
|
+
accent={accent}
|
|
305
|
+
onClick={onCardClick}
|
|
306
|
+
isDraggable={isDraggable}
|
|
307
|
+
dense={dense}
|
|
308
|
+
/>
|
|
309
|
+
))
|
|
310
|
+
)}
|
|
311
|
+
</SortableContext>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{column.footer ? (
|
|
315
|
+
<div className="border-t border-slate-200 px-3 py-2 text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
|
316
|
+
{column.footer}
|
|
317
|
+
</div>
|
|
318
|
+
) : null}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default function KanbanBoard({
|
|
324
|
+
columns = [],
|
|
325
|
+
cards = [],
|
|
326
|
+
title,
|
|
327
|
+
subtitle,
|
|
328
|
+
accent = "default",
|
|
329
|
+
isDraggable = true,
|
|
330
|
+
dense = false,
|
|
331
|
+
showColumnCounts = true,
|
|
332
|
+
showWipLimits = true,
|
|
333
|
+
emptyMessage = "No items",
|
|
334
|
+
loading = false,
|
|
335
|
+
error,
|
|
336
|
+
actions,
|
|
337
|
+
onCardClick,
|
|
338
|
+
onCardMove,
|
|
339
|
+
className = "",
|
|
340
|
+
...cardProps
|
|
341
|
+
}) {
|
|
342
|
+
const [activeId, setActiveId] = useState(null);
|
|
343
|
+
const [internalCards, setInternalCards] = useState(null);
|
|
344
|
+
|
|
345
|
+
const liveCards = internalCards ?? cards;
|
|
346
|
+
|
|
347
|
+
const sensors = useSensors(
|
|
348
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
349
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const cardsByColumn = useMemo(() => {
|
|
353
|
+
const map = {};
|
|
354
|
+
for (const col of columns) map[col.id] = [];
|
|
355
|
+
for (const c of liveCards) {
|
|
356
|
+
const colId = c.columnId;
|
|
357
|
+
if (!map[colId]) map[colId] = [];
|
|
358
|
+
map[colId].push(c);
|
|
359
|
+
}
|
|
360
|
+
return map;
|
|
361
|
+
}, [columns, liveCards]);
|
|
362
|
+
|
|
363
|
+
const findColumnIdForCard = useCallback(
|
|
364
|
+
(cardId) => {
|
|
365
|
+
const card = liveCards.find((c) => c.id === cardId);
|
|
366
|
+
return card?.columnId ?? null;
|
|
367
|
+
},
|
|
368
|
+
[liveCards]
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const resolveColumnId = useCallback(
|
|
372
|
+
(id) => {
|
|
373
|
+
if (!id) return null;
|
|
374
|
+
if (typeof id === "string" && id.startsWith("column:")) return id.slice("column:".length);
|
|
375
|
+
const card = liveCards.find((c) => c.id === id);
|
|
376
|
+
return card?.columnId ?? null;
|
|
377
|
+
},
|
|
378
|
+
[liveCards]
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const handleDragStart = (event) => setActiveId(event.active.id);
|
|
382
|
+
|
|
383
|
+
const handleDragEnd = (event) => {
|
|
384
|
+
setActiveId(null);
|
|
385
|
+
const { active, over } = event;
|
|
386
|
+
if (!over) return;
|
|
387
|
+
|
|
388
|
+
const fromColumnId = findColumnIdForCard(active.id);
|
|
389
|
+
const toColumnId = resolveColumnId(over.id);
|
|
390
|
+
if (!fromColumnId || !toColumnId) return;
|
|
391
|
+
|
|
392
|
+
const fromList = (cardsByColumn[fromColumnId] ?? []).slice();
|
|
393
|
+
const toList =
|
|
394
|
+
fromColumnId === toColumnId ? fromList : (cardsByColumn[toColumnId] ?? []).slice();
|
|
395
|
+
|
|
396
|
+
const fromIndex = fromList.findIndex((c) => c.id === active.id);
|
|
397
|
+
let toIndex;
|
|
398
|
+
|
|
399
|
+
if (over.data?.current?.type === "column" || over.id === `column:${toColumnId}`) {
|
|
400
|
+
toIndex = toList.length;
|
|
401
|
+
} else {
|
|
402
|
+
toIndex = toList.findIndex((c) => c.id === over.id);
|
|
403
|
+
if (toIndex === -1) toIndex = toList.length;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (fromColumnId === toColumnId && fromIndex === toIndex) return;
|
|
407
|
+
|
|
408
|
+
let nextCards;
|
|
409
|
+
if (fromColumnId === toColumnId) {
|
|
410
|
+
const reordered = arrayMove(fromList, fromIndex, toIndex);
|
|
411
|
+
const others = liveCards.filter((c) => c.columnId !== fromColumnId);
|
|
412
|
+
nextCards = [...others, ...reordered];
|
|
413
|
+
} else {
|
|
414
|
+
const moved = { ...fromList[fromIndex], columnId: toColumnId };
|
|
415
|
+
const newFrom = fromList.filter((c) => c.id !== active.id);
|
|
416
|
+
const newTo = toList.slice();
|
|
417
|
+
newTo.splice(toIndex, 0, moved);
|
|
418
|
+
const others = liveCards.filter(
|
|
419
|
+
(c) => c.columnId !== fromColumnId && c.columnId !== toColumnId
|
|
420
|
+
);
|
|
421
|
+
nextCards = [...others, ...newFrom, ...newTo];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (onCardMove) {
|
|
425
|
+
onCardMove({
|
|
426
|
+
cardId: active.id,
|
|
427
|
+
fromColumnId,
|
|
428
|
+
toColumnId,
|
|
429
|
+
fromIndex,
|
|
430
|
+
toIndex,
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
setInternalCards(nextCards);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const activeCard = activeId ? liveCards.find((c) => c.id === activeId) : null;
|
|
438
|
+
|
|
439
|
+
const header =
|
|
440
|
+
title || subtitle || actions ? (
|
|
441
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
442
|
+
<div className="min-w-0">
|
|
443
|
+
{title ? (
|
|
444
|
+
<UIText as="div" size="sm" weight="medium">
|
|
445
|
+
{title}
|
|
446
|
+
</UIText>
|
|
447
|
+
) : null}
|
|
448
|
+
{subtitle ? (
|
|
449
|
+
<UIText as="div" size="xs" muted className="mt-1">
|
|
450
|
+
{subtitle}
|
|
451
|
+
</UIText>
|
|
452
|
+
) : null}
|
|
453
|
+
</div>
|
|
454
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
455
|
+
</div>
|
|
456
|
+
) : null;
|
|
457
|
+
|
|
458
|
+
const board = (
|
|
459
|
+
<div className="flex gap-3 overflow-x-auto pb-1">
|
|
460
|
+
{columns.map((col) => (
|
|
461
|
+
<KanbanColumn
|
|
462
|
+
key={col.id}
|
|
463
|
+
column={col}
|
|
464
|
+
cards={cardsByColumn[col.id] ?? []}
|
|
465
|
+
accent={accent}
|
|
466
|
+
onCardClick={onCardClick}
|
|
467
|
+
isDraggable={isDraggable}
|
|
468
|
+
dense={dense}
|
|
469
|
+
showCounts={showColumnCounts}
|
|
470
|
+
showLimits={showWipLimits}
|
|
471
|
+
emptyMessage={emptyMessage}
|
|
472
|
+
/>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<BaseCard
|
|
479
|
+
padding="default"
|
|
480
|
+
isLoading={loading}
|
|
481
|
+
className={className}
|
|
482
|
+
{...cardProps}
|
|
483
|
+
>
|
|
484
|
+
{header}
|
|
485
|
+
{error ? (
|
|
486
|
+
<div className="rounded-md border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300">
|
|
487
|
+
{String(error)}
|
|
488
|
+
</div>
|
|
489
|
+
) : (
|
|
490
|
+
<DndContext
|
|
491
|
+
sensors={sensors}
|
|
492
|
+
collisionDetection={closestCenter}
|
|
493
|
+
onDragStart={handleDragStart}
|
|
494
|
+
onDragEnd={handleDragEnd}
|
|
495
|
+
onDragCancel={() => setActiveId(null)}
|
|
496
|
+
>
|
|
497
|
+
{board}
|
|
498
|
+
<DragOverlay>
|
|
499
|
+
{activeCard ? (
|
|
500
|
+
<KanbanCardBody card={activeCard} accent={accent} dragging dense={dense} />
|
|
501
|
+
) : null}
|
|
502
|
+
</DragOverlay>
|
|
503
|
+
</DndContext>
|
|
504
|
+
)}
|
|
505
|
+
</BaseCard>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
@@ -29,6 +29,7 @@ export { default as ActivityCard } from "./cards/ActivityCard";
|
|
|
29
29
|
export { default as MetricsStrip } from "./cards/MetricsStrip";
|
|
30
30
|
export { default as CalloutCard } from "./cards/CalloutCard";
|
|
31
31
|
export { default as ActionList } from "./cards/ActionList";
|
|
32
|
+
export { default as KanbanBoard } from "./cards/KanbanBoard";
|
|
32
33
|
|
|
33
34
|
// Charts
|
|
34
35
|
export { default as D3Chart } from "./charts/D3Chart";
|
package/src/styles/global.css
CHANGED
|
@@ -58,31 +58,18 @@
|
|
|
58
58
|
--color-sidebar-border: var(--sidebar-border);
|
|
59
59
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
60
60
|
|
|
61
|
-
/*
|
|
62
|
-
--color-
|
|
63
|
-
--color-
|
|
64
|
-
--color-
|
|
65
|
-
--color-
|
|
66
|
-
--color-
|
|
67
|
-
--color-
|
|
68
|
-
--color-
|
|
69
|
-
--color-
|
|
70
|
-
--color-
|
|
71
|
-
--color-
|
|
72
|
-
--color-
|
|
73
|
-
|
|
74
|
-
/* Engine Cyan brand ramp */
|
|
75
|
-
--color-brand-50: #F0FAFB;
|
|
76
|
-
--color-brand-100: #D9F2F5;
|
|
77
|
-
--color-brand-200: #B3E5EB;
|
|
78
|
-
--color-brand-300: #7DCBD9;
|
|
79
|
-
--color-brand-400: #5BB8CA;
|
|
80
|
-
--color-brand-500: #3AA0B5;
|
|
81
|
-
--color-brand-600: #2D849A;
|
|
82
|
-
--color-brand-700: #266B7E;
|
|
83
|
-
--color-brand-800: #235768;
|
|
84
|
-
--color-brand-900: #1F4858;
|
|
85
|
-
--color-brand-950: #112E3A;
|
|
61
|
+
/* Neutral brand ramp — override per-app to apply your brand colors */
|
|
62
|
+
--color-brand-50: #f8fafc;
|
|
63
|
+
--color-brand-100: #f1f5f9;
|
|
64
|
+
--color-brand-200: #e2e8f0;
|
|
65
|
+
--color-brand-300: #cbd5e1;
|
|
66
|
+
--color-brand-400: #94a3b8;
|
|
67
|
+
--color-brand-500: #64748b;
|
|
68
|
+
--color-brand-600: #475569;
|
|
69
|
+
--color-brand-700: #334155;
|
|
70
|
+
--color-brand-800: #1e293b;
|
|
71
|
+
--color-brand-900: #0f172a;
|
|
72
|
+
--color-brand-950: #020617;
|
|
86
73
|
|
|
87
74
|
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
88
75
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
@@ -92,22 +79,22 @@
|
|
|
92
79
|
}
|
|
93
80
|
|
|
94
81
|
:root {
|
|
95
|
-
--dash-text: #
|
|
96
|
-
--dash-muted: #
|
|
97
|
-
--dash-label: #
|
|
98
|
-
--dash-surface: #
|
|
99
|
-
--dash-border: #
|
|
100
|
-
--dash-accent: #
|
|
82
|
+
--dash-text: #0f172a;
|
|
83
|
+
--dash-muted: #475569;
|
|
84
|
+
--dash-label: #94a3b8;
|
|
85
|
+
--dash-surface: #ffffff;
|
|
86
|
+
--dash-border: #e2e8f0;
|
|
87
|
+
--dash-accent: #64748b;
|
|
101
88
|
--dash-success: #34d399;
|
|
102
89
|
--dash-info: #67e8f9;
|
|
103
|
-
--dash-warning: #
|
|
90
|
+
--dash-warning: #f59e0b;
|
|
104
91
|
--dash-danger: #dc2626;
|
|
105
|
-
--dash-dark: #
|
|
106
|
-
--dash-darker: #
|
|
107
|
-
--dash-chart-1: #
|
|
108
|
-
--dash-chart-2: #
|
|
109
|
-
--dash-chart-3: #
|
|
110
|
-
--dash-chart-4: #
|
|
92
|
+
--dash-dark: #0f172a;
|
|
93
|
+
--dash-darker: #020617;
|
|
94
|
+
--dash-chart-1: #64748b;
|
|
95
|
+
--dash-chart-2: #34d399;
|
|
96
|
+
--dash-chart-3: #67e8f9;
|
|
97
|
+
--dash-chart-4: #f59e0b;
|
|
111
98
|
--dash-metric-size: 2.5rem;
|
|
112
99
|
--dash-metric-sub: 2.25rem;
|
|
113
100
|
--color-dash-text: var(--dash-text);
|
|
@@ -206,37 +193,37 @@
|
|
|
206
193
|
|
|
207
194
|
/*
|
|
208
195
|
* Restore HeroUI theme variables inside the Command Center scope.
|
|
209
|
-
*
|
|
196
|
+
* Neutral defaults — override per-app to apply your brand colors.
|
|
210
197
|
*/
|
|
211
198
|
.heroui-scope {
|
|
212
|
-
--primary:
|
|
199
|
+
--primary: oklch(0.205 0 0);
|
|
213
200
|
--primary-foreground: oklch(0.9911 0 0);
|
|
214
|
-
--secondary:
|
|
215
|
-
--secondary-foreground:
|
|
216
|
-
--success: #
|
|
201
|
+
--secondary: oklch(0.55 0.02 240);
|
|
202
|
+
--secondary-foreground: oklch(0.9911 0 0);
|
|
203
|
+
--success: #16a34a;
|
|
217
204
|
--success-foreground: oklch(0.9911 0 0);
|
|
218
|
-
--warning: #
|
|
219
|
-
--warning-foreground:
|
|
220
|
-
--danger: #
|
|
205
|
+
--warning: #f59e0b;
|
|
206
|
+
--warning-foreground: oklch(0.205 0 0);
|
|
207
|
+
--danger: #dc2626;
|
|
221
208
|
--danger-foreground: oklch(0.9911 0 0);
|
|
222
209
|
|
|
223
210
|
--muted: oklch(0.5517 0.0138 285.94);
|
|
224
211
|
--accent: oklch(0.6204 0.195 253.83);
|
|
225
212
|
--accent-foreground: oklch(0.9911 0 0);
|
|
226
|
-
--background:
|
|
227
|
-
--foreground:
|
|
213
|
+
--background: oklch(0.985 0 0);
|
|
214
|
+
--foreground: oklch(0.145 0 0);
|
|
228
215
|
--default: oklch(94% 0.001 286.375);
|
|
229
|
-
--default-foreground:
|
|
230
|
-
--border:
|
|
216
|
+
--default-foreground: oklch(0.145 0 0);
|
|
217
|
+
--border: oklch(0.85 0.005 286);
|
|
231
218
|
--separator: oklch(92% 0.004 286.32);
|
|
232
219
|
--segment: oklch(100% 0 0);
|
|
233
|
-
--segment-foreground:
|
|
220
|
+
--segment-foreground: oklch(0.145 0 0);
|
|
234
221
|
--surface: oklch(100% 0 0);
|
|
235
|
-
--surface-foreground:
|
|
222
|
+
--surface-foreground: oklch(0.145 0 0);
|
|
236
223
|
--overlay: oklch(100% 0 0);
|
|
237
|
-
--overlay-foreground:
|
|
238
|
-
--focus:
|
|
239
|
-
--link:
|
|
224
|
+
--overlay-foreground: oklch(0.145 0 0);
|
|
225
|
+
--focus: oklch(0.6 0.18 250);
|
|
226
|
+
--link: oklch(0.205 0 0);
|
|
240
227
|
}
|
|
241
228
|
|
|
242
229
|
/* ChatBar expanded overlay — horizontal padding so it doesn't hit window edges */
|
|
@@ -261,9 +248,9 @@ body > .fixed.inset-x-0.rounded-2xl {
|
|
|
261
248
|
--overlay: oklch(0.2103 0.0059 285.89);
|
|
262
249
|
--overlay-foreground: oklch(0.9911 0 0);
|
|
263
250
|
--warning: oklch(0.8203 0.1388 76.34);
|
|
264
|
-
--warning-foreground:
|
|
251
|
+
--warning-foreground: oklch(0.145 0 0);
|
|
265
252
|
--danger: oklch(0.594 0.1967 24.63);
|
|
266
253
|
--danger-foreground: oklch(0.9911 0 0);
|
|
267
|
-
--focus:
|
|
254
|
+
--focus: oklch(0.6 0.18 250);
|
|
268
255
|
--link: oklch(0.9911 0 0);
|
|
269
256
|
}
|