@ciridae/ui 0.3.2 → 0.3.5
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/dist/assets/ciridae/brand-background-dark.webp +0 -0
- package/dist/assets/ciridae/brand-background-light.webp +0 -0
- package/dist/assets/ciridae/ciridae-black.svg +17 -0
- package/dist/assets/ciridae/ciridae-brand-assets.d.ts +25 -0
- package/dist/assets/ciridae/ciridae-brand-assets.js +25 -0
- package/dist/assets/ciridae/ciridae-icon-black.png +0 -0
- package/dist/assets/ciridae/ciridae-icon-white.png +0 -0
- package/dist/assets/ciridae/ciridae-ui-wordmark-black.png +0 -0
- package/dist/assets/ciridae/ciridae-ui-wordmark-white.png +0 -0
- package/dist/assets/ciridae/ciridae-white.svg +17 -0
- package/dist/assets/ciridae/ciridae-wordmark-black.png +0 -0
- package/dist/assets/ciridae/ciridae-wordmark-white.png +0 -0
- package/dist/assets/ciridae/favicon.ico +0 -0
- package/dist/assets/ciridae/logo-white.png +0 -0
- package/dist/assets/ciridae/sign-in-screenshot.png +0 -0
- package/dist/components/blocks/admin-users-table.d.ts +22 -0
- package/dist/components/blocks/admin-users-table.js +115 -0
- package/dist/components/blocks/comment-composer.d.ts +16 -0
- package/dist/components/blocks/comment-composer.js +113 -0
- package/dist/components/blocks/comment-item.d.ts +8 -0
- package/dist/components/blocks/comment-item.js +45 -0
- package/dist/components/blocks/comment-thread.d.ts +18 -0
- package/dist/components/blocks/comment-thread.js +30 -0
- package/dist/components/blocks/comment-types.d.ts +18 -0
- package/dist/components/blocks/comment-types.js +11 -0
- package/dist/components/blocks/feedback-center-comments.d.ts +12 -0
- package/dist/components/blocks/feedback-center-comments.js +11 -0
- package/dist/components/blocks/feedback-center-sheet-parts.d.ts +34 -0
- package/dist/components/blocks/feedback-center-sheet-parts.js +44 -0
- package/dist/components/blocks/feedback-center-sheet.d.ts +19 -0
- package/dist/components/blocks/feedback-center-sheet.js +46 -0
- package/dist/components/blocks/feedback-composer-parts.d.ts +16 -0
- package/dist/components/blocks/feedback-composer-parts.js +43 -0
- package/dist/components/blocks/feedback-composer-types.d.ts +11 -0
- package/dist/components/blocks/feedback-composer-types.js +1 -0
- package/dist/components/blocks/feedback-composer.d.ts +17 -0
- package/dist/components/blocks/feedback-composer.js +112 -0
- package/dist/components/blocks/feedback-date-format.d.ts +5 -0
- package/dist/components/blocks/feedback-date-format.js +11 -0
- package/dist/components/blocks/feedback-detail-sheet.d.ts +9 -0
- package/dist/components/blocks/feedback-detail-sheet.js +41 -0
- package/dist/components/blocks/feedback-metrics.d.ts +6 -0
- package/dist/components/blocks/feedback-metrics.js +15 -0
- package/dist/components/blocks/feedback-sheet.d.ts +8 -0
- package/dist/components/blocks/feedback-sheet.js +16 -0
- package/dist/components/blocks/feedback-status-badge.d.ts +6 -0
- package/dist/components/blocks/feedback-status-badge.js +14 -0
- package/dist/components/blocks/feedback-table-row.d.ts +6 -0
- package/dist/components/blocks/feedback-table-row.js +37 -0
- package/dist/components/blocks/feedback-table-sort.d.ts +11 -0
- package/dist/components/blocks/feedback-table-sort.js +81 -0
- package/dist/components/blocks/feedback-table.d.ts +16 -0
- package/dist/components/blocks/feedback-table.js +45 -0
- package/dist/components/blocks/feedback-tracking-types.d.ts +78 -0
- package/dist/components/blocks/feedback-tracking-types.js +36 -0
- package/dist/components/blocks/feedback-type-badge.d.ts +6 -0
- package/dist/components/blocks/feedback-type-badge.js +12 -0
- package/dist/components/blocks/feedback-user-detail.d.ts +10 -0
- package/dist/components/blocks/feedback-user-detail.js +57 -0
- package/dist/components/blocks/feedback-user-list.d.ts +9 -0
- package/dist/components/blocks/feedback-user-list.js +33 -0
- package/dist/components/blocks/invite-user-sheet.d.ts +20 -0
- package/dist/components/blocks/invite-user-sheet.js +72 -0
- package/dist/components/blocks/kanban-board-parts.d.ts +26 -0
- package/dist/components/blocks/kanban-board-parts.js +85 -0
- package/dist/components/blocks/kanban-board-utils.d.ts +42 -0
- package/dist/components/blocks/kanban-board-utils.js +115 -0
- package/dist/components/blocks/kanban-board.d.ts +16 -0
- package/dist/components/blocks/kanban-board.js +36 -0
- package/dist/components/blocks/kanban-card.d.ts +10 -0
- package/dist/components/blocks/kanban-card.js +34 -0
- package/dist/components/blocks/kanban-column.d.ts +17 -0
- package/dist/components/blocks/kanban-column.js +34 -0
- package/dist/components/blocks/kanban-detail-sheet.d.ts +17 -0
- package/dist/components/blocks/kanban-detail-sheet.js +34 -0
- package/dist/components/blocks/kanban-types.d.ts +24 -0
- package/dist/components/blocks/kanban-types.js +1 -0
- package/dist/components/blocks/timestamp-format.d.ts +8 -0
- package/dist/components/blocks/timestamp-format.js +66 -0
- package/dist/components/pages/admin-users-page.d.ts +11 -0
- package/dist/components/pages/admin-users-page.js +30 -0
- package/dist/components/pages/feedback-stakeholder-page-parts.d.ts +18 -0
- package/dist/components/pages/feedback-stakeholder-page-parts.js +36 -0
- package/dist/components/pages/feedback-stakeholder-page.d.ts +9 -0
- package/dist/components/pages/feedback-stakeholder-page.js +66 -0
- package/dist/components/pages/kanban-page.d.ts +13 -0
- package/dist/components/pages/kanban-page.js +68 -0
- package/dist/components/ui/calendar.d.ts +2 -1
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/dialog.d.ts +2 -1
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/page-header.d.ts +15 -1
- package/dist/components/ui/page-header.js +20 -2
- package/dist/components/ui/resizable.d.ts +9 -2
- package/dist/components/ui/resizable.js +3 -3
- package/dist/components/ui/sidebar-account-menu.d.ts +3 -1
- package/dist/components/ui/sidebar-account-menu.js +5 -5
- package/dist/components/ui/sonner.d.ts +2 -2
- package/dist/components/ui/sonner.js +2 -2
- package/dist/styled-system/css/css.d.ts +11 -16
- package/dist/styled-system/css/cva.d.ts +2 -4
- package/dist/styled-system/css/cx.d.ts +2 -3
- package/dist/styled-system/css/index.d.ts +1 -1
- package/dist/styled-system/css/sva.d.ts +1 -2
- package/dist/styled-system/patterns/aspect-ratio.d.ts +5 -9
- package/dist/styled-system/patterns/bleed.d.ts +6 -10
- package/dist/styled-system/patterns/box.d.ts +4 -9
- package/dist/styled-system/patterns/center.d.ts +5 -9
- package/dist/styled-system/patterns/circle.d.ts +5 -9
- package/dist/styled-system/patterns/container.d.ts +4 -9
- package/dist/styled-system/patterns/cq.d.ts +6 -10
- package/dist/styled-system/patterns/divider.d.ts +7 -11
- package/dist/styled-system/patterns/flex.d.ts +11 -15
- package/dist/styled-system/patterns/float.d.ts +8 -12
- package/dist/styled-system/patterns/grid-item.d.ts +10 -14
- package/dist/styled-system/patterns/grid.d.ts +9 -13
- package/dist/styled-system/patterns/hstack.d.ts +6 -10
- package/dist/styled-system/patterns/index.d.ts +1 -1
- package/dist/styled-system/patterns/link-overlay.d.ts +4 -9
- package/dist/styled-system/patterns/spacer.d.ts +5 -9
- package/dist/styled-system/patterns/square.d.ts +5 -9
- package/dist/styled-system/patterns/stack.d.ts +8 -12
- package/dist/styled-system/patterns/visually-hidden.d.ts +4 -9
- package/dist/styled-system/patterns/vstack.d.ts +6 -10
- package/dist/styled-system/patterns/wrap.d.ts +9 -13
- package/dist/styled-system/tokens/index.d.ts +4 -6
- package/dist/styled-system/tokens/tokens.d.ts +42 -61
- package/dist/styled-system/types/composition.d.ts +20 -211
- package/dist/styled-system/types/conditions.d.ts +297 -303
- package/dist/styled-system/types/csstype.d.ts +20289 -22518
- package/dist/styled-system/types/global.d.ts +11 -12
- package/dist/styled-system/types/index.d.ts +2 -2
- package/dist/styled-system/types/parts.d.ts +2 -3
- package/dist/styled-system/types/pattern.d.ts +75 -71
- package/dist/styled-system/types/prop-type.d.ts +236 -233
- package/dist/styled-system/types/recipe.d.ts +114 -155
- package/dist/styled-system/types/selectors.d.ts +8 -58
- package/dist/styled-system/types/static-css.d.ts +40 -47
- package/dist/styled-system/types/style-props.d.ts +8078 -8082
- package/dist/styled-system/types/system-types.d.ts +69 -111
- package/dist/styles.css +490 -1
- package/package.json +159 -7
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function getKanbanBoardState(columns, cards) {
|
|
2
|
+
const cardsByColumnId = new Map();
|
|
3
|
+
const cardById = new Map();
|
|
4
|
+
const columnIds = new Set(columns.map((column) => column.id));
|
|
5
|
+
for (const column of columns) {
|
|
6
|
+
cardsByColumnId.set(column.id, []);
|
|
7
|
+
}
|
|
8
|
+
for (const card of cards) {
|
|
9
|
+
cardById.set(card.id, card);
|
|
10
|
+
if (import.meta.env.DEV && !columnIds.has(card.columnId)) {
|
|
11
|
+
console.warn(`KanbanBoard received card "${card.id}" for missing column "${card.columnId}".`);
|
|
12
|
+
}
|
|
13
|
+
if (!cardsByColumnId.has(card.columnId))
|
|
14
|
+
cardsByColumnId.set(card.columnId, []);
|
|
15
|
+
cardsByColumnId.get(card.columnId)?.push(card);
|
|
16
|
+
}
|
|
17
|
+
return { cardById, cardsByColumnId };
|
|
18
|
+
}
|
|
19
|
+
function getDragStartHandler({ cardById, setters, suppressSelectRef, }) {
|
|
20
|
+
return (event) => {
|
|
21
|
+
const card = cardById.get(String(event.active.id));
|
|
22
|
+
setters.setActiveCard(card);
|
|
23
|
+
setters.setActiveCardRect(getActiveCardRect(event));
|
|
24
|
+
setters.setActiveMove(undefined);
|
|
25
|
+
setters.setOverColumnId(card?.columnId);
|
|
26
|
+
suppressSelectRef.current = true;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getDragOverHandler({ boardState, setActiveMove, setOverColumnId, }) {
|
|
30
|
+
return (event) => {
|
|
31
|
+
const moveEvent = getKanbanMoveEvent(event.active.id, event.over?.id, boardState);
|
|
32
|
+
const nextOverColumnId = moveEvent?.toColumnId ?? getTargetColumnId(event.over?.id, boardState);
|
|
33
|
+
setActiveMove((currentMove) => areKanbanMovesEqual(currentMove, moveEvent) ? currentMove : moveEvent);
|
|
34
|
+
setOverColumnId((currentColumnId) => currentColumnId === nextOverColumnId ? currentColumnId : nextOverColumnId);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getDragEndHandler({ activeMove, boardState, onCardMove, setters, suppressSelectRef, }) {
|
|
38
|
+
return (event) => {
|
|
39
|
+
const moveEvent = getKanbanMoveEvent(event.active.id, event.over?.id, boardState) ?? activeMove;
|
|
40
|
+
clearDragState(setters);
|
|
41
|
+
window.setTimeout(() => {
|
|
42
|
+
suppressSelectRef.current = false;
|
|
43
|
+
}, 0);
|
|
44
|
+
if (moveEvent)
|
|
45
|
+
onCardMove?.(moveEvent);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getKanbanMoveEvent(activeId, overId, boardState) {
|
|
49
|
+
const activeCard = boardState.cardById.get(String(activeId));
|
|
50
|
+
if (!activeCard || !overId)
|
|
51
|
+
return undefined;
|
|
52
|
+
const targetColumnId = getTargetColumnId(overId, boardState);
|
|
53
|
+
if (!targetColumnId)
|
|
54
|
+
return undefined;
|
|
55
|
+
const fromCards = boardState.cardsByColumnId.get(activeCard.columnId) ?? [];
|
|
56
|
+
const targetCards = boardState.cardsByColumnId.get(targetColumnId) ?? [];
|
|
57
|
+
const fromIndex = fromCards.findIndex((card) => card.id === activeCard.id);
|
|
58
|
+
const toIndex = getTargetIndex(overId, targetCards);
|
|
59
|
+
if (fromIndex < 0 || toIndex < 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (activeCard.columnId === targetColumnId && fromIndex === toIndex)
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
cardId: activeCard.id,
|
|
65
|
+
fromColumnId: activeCard.columnId,
|
|
66
|
+
fromIndex,
|
|
67
|
+
toColumnId: targetColumnId,
|
|
68
|
+
toIndex,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function clearDragState(setters) {
|
|
72
|
+
setters.setActiveCard(undefined);
|
|
73
|
+
setters.setActiveCardRect(undefined);
|
|
74
|
+
setters.setActiveMove(undefined);
|
|
75
|
+
setters.setOverColumnId(undefined);
|
|
76
|
+
}
|
|
77
|
+
function getColumnDroppableId(columnId) {
|
|
78
|
+
return `${columnDroppablePrefix}${columnId}`;
|
|
79
|
+
}
|
|
80
|
+
function getTargetColumnId(overId, boardState) {
|
|
81
|
+
if (!overId)
|
|
82
|
+
return undefined;
|
|
83
|
+
const id = String(overId);
|
|
84
|
+
if (isColumnDroppableId(id))
|
|
85
|
+
return id.replace(columnDroppablePrefix, "");
|
|
86
|
+
return boardState.cardById.get(id)?.columnId;
|
|
87
|
+
}
|
|
88
|
+
function getTargetIndex(overId, targetCards) {
|
|
89
|
+
const id = String(overId);
|
|
90
|
+
if (isColumnDroppableId(id))
|
|
91
|
+
return targetCards.length;
|
|
92
|
+
return targetCards.findIndex((card) => card.id === id);
|
|
93
|
+
}
|
|
94
|
+
function getActiveCardRect(event) {
|
|
95
|
+
const rect = event.active.rect.current.initial;
|
|
96
|
+
if (!rect)
|
|
97
|
+
return undefined;
|
|
98
|
+
return { height: rect.height, width: rect.width };
|
|
99
|
+
}
|
|
100
|
+
function isColumnDroppableId(id) {
|
|
101
|
+
return id.startsWith(columnDroppablePrefix);
|
|
102
|
+
}
|
|
103
|
+
function areKanbanMovesEqual(currentMove, nextMove) {
|
|
104
|
+
if (!currentMove && !nextMove)
|
|
105
|
+
return true;
|
|
106
|
+
if (!currentMove || !nextMove)
|
|
107
|
+
return false;
|
|
108
|
+
return (currentMove.cardId === nextMove.cardId &&
|
|
109
|
+
currentMove.fromColumnId === nextMove.fromColumnId &&
|
|
110
|
+
currentMove.toColumnId === nextMove.toColumnId &&
|
|
111
|
+
currentMove.fromIndex === nextMove.fromIndex &&
|
|
112
|
+
currentMove.toIndex === nextMove.toIndex);
|
|
113
|
+
}
|
|
114
|
+
const columnDroppablePrefix = "kanban-column:";
|
|
115
|
+
export { clearDragState, getColumnDroppableId, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { KanbanCardData, KanbanColumnData, KanbanMoveEvent } from "@ciridae/ui/kanban-types";
|
|
3
|
+
type KanbanBoardProps = {
|
|
4
|
+
cards: KanbanCardData[];
|
|
5
|
+
columns: KanbanColumnData[];
|
|
6
|
+
columnMaxHeight?: React.CSSProperties["maxHeight"];
|
|
7
|
+
empty?: React.ReactNode;
|
|
8
|
+
isLoading?: boolean;
|
|
9
|
+
onCardMove?: (event: KanbanMoveEvent) => void;
|
|
10
|
+
onCardSelect?: (card: KanbanCardData) => void;
|
|
11
|
+
renderCard?: (card: KanbanCardData) => React.ReactNode;
|
|
12
|
+
renderCardActions?: (card: KanbanCardData) => React.ReactNode;
|
|
13
|
+
renderColumnActions?: (column: KanbanColumnData) => React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
declare function KanbanBoard({ cards, columns, columnMaxHeight, empty, isLoading, onCardMove, onCardSelect, renderCard, renderCardActions, renderColumnActions, }: KanbanBoardProps): React.JSX.Element;
|
|
16
|
+
export { KanbanBoard, type KanbanBoardProps };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react-hooks/refs -- dnd-kit drag handlers need a mutable ref to suppress the click fired after a drag. */
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { closestCorners, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
|
|
5
|
+
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
|
6
|
+
import { KanbanBoardColumn, KanbanDragOverlay } from "@ciridae/ui/kanban-board-parts";
|
|
7
|
+
import { clearDragState, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, } from "@ciridae/ui/kanban-board-utils";
|
|
8
|
+
function KanbanBoard({ cards, columns, columnMaxHeight = "min(44rem, calc(100vh - 14rem))", empty, isLoading = false, onCardMove, onCardSelect, renderCard, renderCardActions, renderColumnActions, }) {
|
|
9
|
+
const [activeCard, setActiveCard] = React.useState();
|
|
10
|
+
const [activeCardRect, setActiveCardRect] = React.useState();
|
|
11
|
+
const [activeMove, setActiveMove] = React.useState();
|
|
12
|
+
const [overColumnId, setOverColumnId] = React.useState();
|
|
13
|
+
const suppressSelectRef = React.useRef(false);
|
|
14
|
+
const boardState = React.useMemo(() => getKanbanBoardState(columns, cards), [cards, columns]);
|
|
15
|
+
const dragStateSetters = React.useMemo(() => ({ setActiveCard, setActiveCardRect, setActiveMove, setOverColumnId }), []);
|
|
16
|
+
const sensors = useKanbanSensors();
|
|
17
|
+
return (_jsxs(DndContext, { collisionDetection: closestCorners, onDragCancel: () => clearDragState(dragStateSetters), onDragEnd: getDragEndHandler({
|
|
18
|
+
activeMove,
|
|
19
|
+
boardState,
|
|
20
|
+
onCardMove,
|
|
21
|
+
setters: dragStateSetters,
|
|
22
|
+
suppressSelectRef,
|
|
23
|
+
}), onDragOver: getDragOverHandler({
|
|
24
|
+
boardState,
|
|
25
|
+
setActiveMove,
|
|
26
|
+
setOverColumnId,
|
|
27
|
+
}), onDragStart: getDragStartHandler({
|
|
28
|
+
cardById: boardState.cardById,
|
|
29
|
+
setters: dragStateSetters,
|
|
30
|
+
suppressSelectRef,
|
|
31
|
+
}), sensors: sensors, children: [_jsx("div", { className: "w-full overflow-x-auto", children: _jsx("div", { className: "flex min-h-80 w-max gap-3 pb-2", children: columns.map((column) => (_jsx(KanbanBoardColumn, { activeCardId: activeCard?.id, activeCardRect: activeCardRect, activeMove: activeMove, boardState: boardState, column: column, columnMaxHeight: columnMaxHeight, empty: empty, isLoading: isLoading, isOver: overColumnId === column.id, onCardMove: onCardMove, onCardSelect: onCardSelect, renderCard: renderCard, renderCardActions: renderCardActions, renderColumnActions: renderColumnActions, suppressSelectRef: suppressSelectRef }, column.id))) }) }), _jsx(KanbanDragOverlay, { activeCard: activeCard, activeCardRect: activeCardRect })] }));
|
|
32
|
+
}
|
|
33
|
+
function useKanbanSensors() {
|
|
34
|
+
return useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
35
|
+
}
|
|
36
|
+
export { KanbanBoard };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { KanbanCardData } from "@ciridae/ui/kanban-types";
|
|
3
|
+
type KanbanCardProps = {
|
|
4
|
+
actionsSlot?: React.ReactNode;
|
|
5
|
+
card: KanbanCardData;
|
|
6
|
+
isDragging?: boolean;
|
|
7
|
+
onSelect?: (card: KanbanCardData) => void;
|
|
8
|
+
};
|
|
9
|
+
declare function KanbanCard({ actionsSlot, card, isDragging, onSelect }: KanbanCardProps): React.JSX.Element;
|
|
10
|
+
export { KanbanCard, type KanbanCardProps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "@ciridae/ui/utils";
|
|
3
|
+
function KanbanCard({ actionsSlot, card, isDragging = false, onSelect }) {
|
|
4
|
+
return (_jsxs("article", { "aria-disabled": card.disabled, "data-dragging": isDragging, "data-disabled": card.disabled, onClick: getCardSelectHandler(card, onSelect), onKeyDown: getCardSelectKeyHandler(card, onSelect), role: onSelect ? "button" : undefined, tabIndex: onSelect && !card.disabled ? 0 : undefined, className: cn("group/kanban-card grid min-w-0 gap-2 rounded-md border bg-card p-3 text-card-foreground shadow-xs transition-[background-color,border-color,box-shadow,opacity,transform] outline-none", "hover:bg-accent/35 focus-visible:border-ring focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50", "data-[dragging=true]:border-ring data-[dragging=true]:bg-accent/40 data-[dragging=true]:opacity-80", "data-[disabled=true]:opacity-60", onSelect && !card.disabled ? "cursor-pointer" : undefined), children: [_jsx(KanbanCardHeader, { actionsSlot: actionsSlot, card: card }), card.description ? (_jsx("p", { className: "line-clamp-2 text-sm leading-normal text-muted-foreground", children: card.description })) : null, _jsx(KanbanCardFooter, { card: card })] }));
|
|
5
|
+
}
|
|
6
|
+
function KanbanCardHeader({ actionsSlot, card, }) {
|
|
7
|
+
return (_jsxs("header", { className: "flex min-w-0 items-start justify-between gap-2", children: [_jsx("h4", { className: "min-w-0 flex-1 text-sm leading-snug font-medium text-foreground", children: card.title }), _jsx("div", { className: "flex shrink-0 items-center gap-1", "data-kanban-card-action": true, onClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: actionsSlot })] }));
|
|
8
|
+
}
|
|
9
|
+
function KanbanCardFooter({ card }) {
|
|
10
|
+
if (!card.badges && !card.meta)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsxs("footer", { className: "flex min-w-0 flex-wrap items-center justify-between gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: card.badges }), card.meta ? (_jsx("div", { className: "min-w-0 truncate text-xs text-muted-foreground", children: card.meta })) : null] }));
|
|
13
|
+
}
|
|
14
|
+
function getCardSelectHandler(card, onSelect) {
|
|
15
|
+
return (event) => {
|
|
16
|
+
if (!onSelect || card.disabled)
|
|
17
|
+
return;
|
|
18
|
+
if (event.target instanceof HTMLElement && event.target.closest("[data-kanban-card-action]")) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
onSelect(card);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function getCardSelectKeyHandler(card, onSelect) {
|
|
25
|
+
return (event) => {
|
|
26
|
+
if (!onSelect || card.disabled)
|
|
27
|
+
return;
|
|
28
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
29
|
+
return;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
onSelect(card);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export { KanbanCard };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { KanbanCardData, KanbanColumnData } from "@ciridae/ui/kanban-types";
|
|
3
|
+
type KanbanColumnProps = {
|
|
4
|
+
actionsSlot?: React.ReactNode;
|
|
5
|
+
cards: KanbanCardData[];
|
|
6
|
+
column: KanbanColumnData;
|
|
7
|
+
columnMaxHeight?: React.CSSProperties["maxHeight"];
|
|
8
|
+
dropPlaceholder?: React.ReactNode;
|
|
9
|
+
dropPlaceholderIndex?: number;
|
|
10
|
+
empty?: React.ReactNode;
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
isOver?: boolean;
|
|
13
|
+
renderCard?: (card: KanbanCardData) => React.ReactNode;
|
|
14
|
+
renderCardActions?: (card: KanbanCardData) => React.ReactNode;
|
|
15
|
+
};
|
|
16
|
+
declare function KanbanColumn({ actionsSlot, cards, column, columnMaxHeight, dropPlaceholder, dropPlaceholderIndex, empty, isLoading, isOver, renderCard, renderCardActions, }: KanbanColumnProps): React.JSX.Element;
|
|
17
|
+
export { KanbanColumn, type KanbanColumnProps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { KanbanCard } from "@ciridae/ui/kanban-card";
|
|
4
|
+
import { Skeleton } from "@ciridae/ui/skeleton";
|
|
5
|
+
import { cn } from "@ciridae/ui/utils";
|
|
6
|
+
function KanbanColumn({ actionsSlot, cards, column, columnMaxHeight, dropPlaceholder, dropPlaceholderIndex, empty, isLoading = false, isOver = false, renderCard, renderCardActions, }) {
|
|
7
|
+
const isOverLimit = isColumnOverLimit(column, cards);
|
|
8
|
+
return (_jsxs("section", { "aria-label": column.title, "data-over": isOver, className: cn("flex min-h-72 min-w-72 flex-col overflow-hidden rounded-lg border bg-muted/25 transition-colors", "data-[over=true]:border-ring data-[over=true]:bg-muted/60"), style: { maxHeight: columnMaxHeight }, children: [_jsx(KanbanColumnHeader, { actionsSlot: actionsSlot, cardCount: cards.length, column: column, isOverLimit: isOverLimit }), _jsx("div", { className: "grid flex-1 content-start gap-2 overflow-y-auto p-2", children: _jsx(KanbanColumnContent, { cards: cards, dropPlaceholder: dropPlaceholder, dropPlaceholderIndex: dropPlaceholderIndex, empty: empty, isLoading: isLoading, renderCard: renderCard, renderCardActions: renderCardActions }) })] }));
|
|
9
|
+
}
|
|
10
|
+
function KanbanColumnHeader({ actionsSlot, cardCount, column, isOverLimit, }) {
|
|
11
|
+
return (_jsx("header", { className: "grid gap-1 border-b bg-background/70 px-3 py-2.5", children: _jsxs("div", { className: "flex min-w-0 items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("h3", { className: "truncate text-sm font-medium text-foreground", children: column.title }), _jsx(KanbanColumnCount, { cardCount: cardCount, isOverLimit: isOverLimit, limit: column.limit })] }), column.description ? (_jsx("p", { className: "mt-1 line-clamp-1 text-xs text-muted-foreground", children: column.description })) : null] }), actionsSlot ? _jsx("div", { className: "shrink-0", children: actionsSlot }) : null] }) }));
|
|
12
|
+
}
|
|
13
|
+
function KanbanColumnCount({ cardCount, isOverLimit, limit, }) {
|
|
14
|
+
const label = typeof limit === "number" ? `${cardCount}/${limit}` : String(cardCount);
|
|
15
|
+
return (_jsx("span", { "data-over-limit": isOverLimit, className: "rounded-sm bg-muted px-1.5 py-0.5 font-mono text-[11px] leading-none text-muted-foreground tabular-nums data-[over-limit=true]:bg-warning-muted data-[over-limit=true]:text-warning", children: label }));
|
|
16
|
+
}
|
|
17
|
+
function KanbanColumnContent({ cards, dropPlaceholder, dropPlaceholderIndex, empty, isLoading, renderCard, renderCardActions, }) {
|
|
18
|
+
if (isLoading)
|
|
19
|
+
return _jsx(KanbanColumnSkeleton, {});
|
|
20
|
+
if (cards.length === 0 && !dropPlaceholder) {
|
|
21
|
+
return _jsx(KanbanColumnEmpty, { children: empty });
|
|
22
|
+
}
|
|
23
|
+
return (_jsxs(_Fragment, { children: [cards.map((card, index) => (_jsxs(React.Fragment, { children: [dropPlaceholderIndex === index ? dropPlaceholder : null, _jsx("div", { children: renderCard ? (renderCard(card)) : (_jsx(KanbanCard, { actionsSlot: renderCardActions?.(card), card: card })) })] }, card.id))), dropPlaceholderIndex === cards.length ? dropPlaceholder : null] }));
|
|
24
|
+
}
|
|
25
|
+
function KanbanColumnEmpty({ children }) {
|
|
26
|
+
return (_jsx("div", { className: "rounded-md border border-dashed bg-background/60 px-3 py-2.5 text-sm text-muted-foreground", children: children ?? "No cards" }));
|
|
27
|
+
}
|
|
28
|
+
function KanbanColumnSkeleton() {
|
|
29
|
+
return (_jsx("div", { className: "grid gap-2", children: Array.from({ length: 3 }).map((_, index) => (_jsxs("div", { className: "rounded-md border bg-card p-3 shadow-xs", children: [_jsx(Skeleton, { className: "h-3.5 w-4/5" }), _jsx(Skeleton, { className: "mt-3 h-3 w-full" }), _jsx(Skeleton, { className: "mt-2 h-3 w-2/3" })] }, index))) }));
|
|
30
|
+
}
|
|
31
|
+
function isColumnOverLimit(column, cards) {
|
|
32
|
+
return typeof column.limit === "number" && cards.length > column.limit;
|
|
33
|
+
}
|
|
34
|
+
export { KanbanColumn };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { KanbanCardData, KanbanColumnData } from "@ciridae/ui/kanban-types";
|
|
3
|
+
type KanbanDetailProperty = {
|
|
4
|
+
label: string;
|
|
5
|
+
value: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
type KanbanDetailSheetProps = {
|
|
8
|
+
actionsSlot?: React.ReactNode;
|
|
9
|
+
activitySlot?: React.ReactNode;
|
|
10
|
+
card?: KanbanCardData;
|
|
11
|
+
column?: KanbanColumnData;
|
|
12
|
+
onOpenChange: (open: boolean) => void;
|
|
13
|
+
open: boolean;
|
|
14
|
+
properties?: KanbanDetailProperty[];
|
|
15
|
+
};
|
|
16
|
+
declare function KanbanDetailSheet({ actionsSlot, activitySlot, card, column, onOpenChange, open, properties, }: KanbanDetailSheetProps): React.JSX.Element;
|
|
17
|
+
export { KanbanDetailSheet, type KanbanDetailProperty, type KanbanDetailSheetProps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Badge } from "@ciridae/ui/badge";
|
|
3
|
+
import { Button } from "@ciridae/ui/button";
|
|
4
|
+
import { Card } from "@ciridae/ui/card";
|
|
5
|
+
import { PropertyList, PropertyListItem, PropertyListLabel, PropertyListValue, } from "@ciridae/ui/property-list";
|
|
6
|
+
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@ciridae/ui/sheet";
|
|
7
|
+
function KanbanDetailSheet({ actionsSlot, activitySlot, card, column, onOpenChange, open, properties = [], }) {
|
|
8
|
+
return (_jsx(Sheet, { onOpenChange: onOpenChange, open: open, children: _jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Card details" }), _jsx(SheetDescription, { children: "Review the selected work item." })] }), _jsx(SheetBody, { className: "grid content-start gap-(--panel-gap) pb-(--panel-padding)", children: card ? (_jsx(KanbanDetailContent, { activitySlot: activitySlot, card: card, column: column, properties: properties })) : null }), _jsxs(SheetFooter, { children: [actionsSlot, _jsx(Button, { onClick: () => onOpenChange(false), type: "button", children: "Done" })] })] }) }));
|
|
9
|
+
}
|
|
10
|
+
function KanbanDetailContent({ activitySlot, card, column, properties, }) {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx(KanbanDetailHeader, { card: card, column: column }), _jsx(KanbanDetailProperties, { card: card, properties: properties }), _jsx(KanbanDetailDescription, { card: card }), activitySlot ? _jsx(KanbanDetailActivity, { children: activitySlot }) : null] }));
|
|
12
|
+
}
|
|
13
|
+
function KanbanDetailHeader({ card, column }) {
|
|
14
|
+
return (_jsx("header", { className: "grid gap-3", children: _jsxs("div", { className: "flex min-w-0 items-center justify-between gap-3", children: [_jsx("h3", { className: "min-w-0 truncate text-sm font-medium text-foreground", children: card.title }), _jsx(KanbanDetailBadges, { card: card, column: column })] }) }));
|
|
15
|
+
}
|
|
16
|
+
function KanbanDetailBadges({ card, column }) {
|
|
17
|
+
return (_jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: [card.badges, _jsx(KanbanStatusBadge, { column: column })] }));
|
|
18
|
+
}
|
|
19
|
+
function KanbanStatusBadge({ column }) {
|
|
20
|
+
return _jsx(Badge, { variant: "outline", children: column?.title ?? "Unassigned" });
|
|
21
|
+
}
|
|
22
|
+
function KanbanDetailProperties({ card, properties, }) {
|
|
23
|
+
return (_jsxs(PropertyList, { children: [_jsx(KanbanProperty, { label: "Reference", children: _jsx("span", { className: "font-mono text-xs tabular-nums", children: card.meta ?? card.id }) }), properties.map((property) => (_jsx(KanbanProperty, { label: property.label, children: property.value }, property.label)))] }));
|
|
24
|
+
}
|
|
25
|
+
function KanbanProperty({ children, label }) {
|
|
26
|
+
return (_jsxs(PropertyListItem, { children: [_jsx(PropertyListLabel, { children: label }), _jsx(PropertyListValue, { children: children })] }));
|
|
27
|
+
}
|
|
28
|
+
function KanbanDetailDescription({ card }) {
|
|
29
|
+
return (_jsxs(Card, { className: "grid gap-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Details" }), _jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: card.description ?? "No description provided." })] }));
|
|
30
|
+
}
|
|
31
|
+
function KanbanDetailActivity({ children }) {
|
|
32
|
+
return (_jsxs(Card, { className: "grid gap-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Activity" }), children] }));
|
|
33
|
+
}
|
|
34
|
+
export { KanbanDetailSheet };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
type KanbanColumnData = {
|
|
3
|
+
description?: string;
|
|
4
|
+
id: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
type KanbanCardData = {
|
|
9
|
+
badges?: React.ReactNode;
|
|
10
|
+
columnId: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
id: string;
|
|
14
|
+
meta?: React.ReactNode;
|
|
15
|
+
title: string;
|
|
16
|
+
};
|
|
17
|
+
type KanbanMoveEvent = {
|
|
18
|
+
cardId: string;
|
|
19
|
+
fromColumnId: string;
|
|
20
|
+
fromIndex: number;
|
|
21
|
+
toColumnId: string;
|
|
22
|
+
toIndex: number;
|
|
23
|
+
};
|
|
24
|
+
export type { KanbanCardData, KanbanColumnData, KanbanMoveEvent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type TimestampFormatOptions = {
|
|
2
|
+
fallback?: string;
|
|
3
|
+
};
|
|
4
|
+
declare function formatTimestamp(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
5
|
+
declare function formatTimestampFull(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
6
|
+
declare function getTimestampDateTime(value: string | undefined): string | undefined;
|
|
7
|
+
export { formatTimestamp, formatTimestampFull, getTimestampDateTime };
|
|
8
|
+
export type { TimestampFormatOptions };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function formatTimestamp(value, options = {}) {
|
|
2
|
+
const date = parseTimestamp(value);
|
|
3
|
+
if (!date)
|
|
4
|
+
return getTimestampFallback(value, options.fallback);
|
|
5
|
+
const time = timeFormatter.format(date);
|
|
6
|
+
const dayDifference = getLocalCalendarDayDifference(date, new Date());
|
|
7
|
+
if (dayDifference === 0)
|
|
8
|
+
return `Today, ${time}`;
|
|
9
|
+
if (dayDifference === -1)
|
|
10
|
+
return `Yesterday, ${time}`;
|
|
11
|
+
if (dayDifference < 0 && dayDifference > -7) {
|
|
12
|
+
return `${weekdayFormatter.format(date)}, ${time}`;
|
|
13
|
+
}
|
|
14
|
+
if (date.getFullYear() === new Date().getFullYear()) {
|
|
15
|
+
return `${monthDayFormatter.format(date)}, ${time}`;
|
|
16
|
+
}
|
|
17
|
+
return dateTimeFormatter.format(date);
|
|
18
|
+
}
|
|
19
|
+
function formatTimestampFull(value, options = {}) {
|
|
20
|
+
const date = parseTimestamp(value);
|
|
21
|
+
if (!date)
|
|
22
|
+
return getTimestampFallback(value, options.fallback);
|
|
23
|
+
return dateTimeFormatter.format(date);
|
|
24
|
+
}
|
|
25
|
+
function getTimestampDateTime(value) {
|
|
26
|
+
const date = parseTimestamp(value);
|
|
27
|
+
if (!date)
|
|
28
|
+
return undefined;
|
|
29
|
+
return date.toISOString();
|
|
30
|
+
}
|
|
31
|
+
function parseTimestamp(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return undefined;
|
|
34
|
+
const timestamp = Date.parse(value);
|
|
35
|
+
if (Number.isNaN(timestamp))
|
|
36
|
+
return undefined;
|
|
37
|
+
return new Date(timestamp);
|
|
38
|
+
}
|
|
39
|
+
function getTimestampFallback(value, fallback = "No update") {
|
|
40
|
+
return value?.trim() || fallback;
|
|
41
|
+
}
|
|
42
|
+
function getLocalCalendarDayDifference(date, comparisonDate) {
|
|
43
|
+
const dateStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
44
|
+
const comparisonStart = new Date(comparisonDate.getFullYear(), comparisonDate.getMonth(), comparisonDate.getDate()).getTime();
|
|
45
|
+
return Math.round((dateStart - comparisonStart) / millisecondsPerDay);
|
|
46
|
+
}
|
|
47
|
+
const millisecondsPerDay = 24 * 60 * 60 * 1000;
|
|
48
|
+
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
49
|
+
hour: "numeric",
|
|
50
|
+
minute: "2-digit",
|
|
51
|
+
});
|
|
52
|
+
const weekdayFormatter = new Intl.DateTimeFormat(undefined, {
|
|
53
|
+
weekday: "short",
|
|
54
|
+
});
|
|
55
|
+
const monthDayFormatter = new Intl.DateTimeFormat(undefined, {
|
|
56
|
+
day: "numeric",
|
|
57
|
+
month: "short",
|
|
58
|
+
});
|
|
59
|
+
const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
60
|
+
day: "numeric",
|
|
61
|
+
hour: "numeric",
|
|
62
|
+
minute: "2-digit",
|
|
63
|
+
month: "short",
|
|
64
|
+
year: "numeric",
|
|
65
|
+
});
|
|
66
|
+
export { formatTimestamp, formatTimestampFull, getTimestampDateTime };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type AdminUsersTableProps } from "@ciridae/ui/admin-users-table";
|
|
3
|
+
import { type InviteUserRole, type InviteUserValues } from "@ciridae/ui/invite-user-sheet";
|
|
4
|
+
type AdminUsersPageProps = AdminUsersTableProps & {
|
|
5
|
+
defaultRole?: string;
|
|
6
|
+
isInviting?: boolean;
|
|
7
|
+
onInviteUser: (values: InviteUserValues) => void | Promise<void>;
|
|
8
|
+
roles: InviteUserRole[];
|
|
9
|
+
};
|
|
10
|
+
declare function AdminUsersPage({ defaultRole, isInviting, onInviteUser, roles, users, ...tableProps }: AdminUsersPageProps): React.JSX.Element;
|
|
11
|
+
export { AdminUsersPage, type AdminUsersPageProps };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { UserPlusIcon } from "lucide-react";
|
|
4
|
+
import { AdminUsersTable, } from "@ciridae/ui/admin-users-table";
|
|
5
|
+
import { InviteUserSheet, } from "@ciridae/ui/invite-user-sheet";
|
|
6
|
+
import { Button } from "@ciridae/ui/button";
|
|
7
|
+
import { MetricsStrip, MetricsStripDetail, MetricsStripItem, MetricsStripLabel, MetricsStripValue, } from "@ciridae/ui/metrics-strip";
|
|
8
|
+
import { PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderMain, PageHeaderTitle, } from "@ciridae/ui/page-header";
|
|
9
|
+
function AdminUsersPage({ defaultRole, isInviting = false, onInviteUser, roles, users, ...tableProps }) {
|
|
10
|
+
const [inviteOpen, setInviteOpen] = React.useState(false);
|
|
11
|
+
return (_jsxs("div", { className: "min-h-svh bg-background text-foreground", children: [_jsx(AdminUsersPageHeader, { onInvite: () => setInviteOpen(true) }), _jsxs("main", { className: "grid gap-4 px-4 py-4 md:px-6 lg:px-8", children: [_jsx(AdminUsersMetrics, { users: users }), _jsx(AdminUsersTable, { users: users, ...tableProps })] }), _jsx(InviteUserSheet, { defaultRole: defaultRole, isSubmitting: isInviting, onOpenChange: setInviteOpen, onSubmit: onInviteUser, open: inviteOpen, roles: roles })] }));
|
|
12
|
+
}
|
|
13
|
+
function AdminUsersPageHeader({ onInvite }) {
|
|
14
|
+
return (_jsx(PageHeader, { children: _jsxs(PageHeaderMain, { children: [_jsxs(PageHeaderContent, { children: [_jsx(PageHeaderTitle, { children: "Admin user management" }), _jsx(PageHeaderDescription, { children: "Manage platform access, pending invites, and user status." })] }), _jsx(PageHeaderActions, { children: _jsxs(Button, { onClick: onInvite, type: "button", children: [_jsx(UserPlusIcon, {}), "Invite user"] }) })] }) }));
|
|
15
|
+
}
|
|
16
|
+
function AdminUsersMetrics({ users }) {
|
|
17
|
+
const metrics = getAdminUserMetrics(users);
|
|
18
|
+
return (_jsxs(MetricsStrip, { className: "md:grid-cols-3", children: [_jsx(AdminUsersMetric, { detail: "Total platform users", label: "Users", value: metrics.total }), _jsx(AdminUsersMetric, { detail: "Invites awaiting acceptance", label: "Pending", value: metrics.invited }), _jsx(AdminUsersMetric, { detail: "Users with admin access", label: "Admins", value: metrics.admins })] }));
|
|
19
|
+
}
|
|
20
|
+
function AdminUsersMetric({ detail, label, value, }) {
|
|
21
|
+
return (_jsxs(MetricsStripItem, { children: [_jsx(MetricsStripLabel, { children: label }), _jsx(MetricsStripValue, { children: value }), _jsx(MetricsStripDetail, { children: detail })] }));
|
|
22
|
+
}
|
|
23
|
+
function getAdminUserMetrics(users) {
|
|
24
|
+
return {
|
|
25
|
+
admins: users.filter((user) => user.role.toLowerCase().includes("admin")).length,
|
|
26
|
+
invited: users.filter((user) => user.status === "invited").length,
|
|
27
|
+
total: users.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export { AdminUsersPage };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type FeedbackStatus, type StakeholderFeedbackType } from "@ciridae/ui/feedback-tracking-types";
|
|
2
|
+
type StatusFilter = FeedbackStatus | "all";
|
|
3
|
+
type TypeFilter = StakeholderFeedbackType | "all";
|
|
4
|
+
type FeedbackFilters = {
|
|
5
|
+
search: string;
|
|
6
|
+
status: StatusFilter;
|
|
7
|
+
type: TypeFilter;
|
|
8
|
+
};
|
|
9
|
+
declare function FeedbackFiltersToolbar({ filters, hasActiveFilters, onClearFilters, onFiltersChange, }: {
|
|
10
|
+
filters: FeedbackFilters;
|
|
11
|
+
hasActiveFilters: boolean;
|
|
12
|
+
onClearFilters: () => void;
|
|
13
|
+
onFiltersChange: (filters: FeedbackFilters) => void;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
declare function ClearFiltersButton({ onClearFilters }: {
|
|
16
|
+
onClearFilters: () => void;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export { ClearFiltersButton, FeedbackFiltersToolbar, type FeedbackFilters, type StatusFilter, type TypeFilter, };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { SearchIcon, XIcon } from "lucide-react";
|
|
3
|
+
import { feedbackStatusOptions, feedbackTypeOptions, } from "@ciridae/ui/feedback-tracking-types";
|
|
4
|
+
import { Button } from "@ciridae/ui/button";
|
|
5
|
+
import { Input } from "@ciridae/ui/input";
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@ciridae/ui/select";
|
|
7
|
+
function FeedbackFiltersToolbar({ filters, hasActiveFilters, onClearFilters, onFiltersChange, }) {
|
|
8
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 md:flex-row md:items-center md:justify-between", children: [_jsx(FeedbackSearchInput, { onSearchChange: (search) => onFiltersChange({ ...filters, search }), search: filters.search }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center", children: [_jsx(FeedbackStatusFilter, { onStatusChange: (status) => onFiltersChange({ ...filters, status }), status: filters.status }), _jsx(FeedbackTypeFilter, { onTypeChange: (type) => onFiltersChange({ ...filters, type }), type: filters.type }), hasActiveFilters ? _jsx(ClearFiltersButton, { onClearFilters: onClearFilters }) : null] })] }));
|
|
9
|
+
}
|
|
10
|
+
function FeedbackSearchInput({ onSearchChange, search, }) {
|
|
11
|
+
return (_jsxs("div", { className: "relative w-full md:max-w-sm", children: [_jsx(SearchIcon, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { "aria-label": "Search feedback", className: "pl-8", onChange: (event) => onSearchChange(event.target.value), placeholder: "Search feedback", value: search })] }));
|
|
12
|
+
}
|
|
13
|
+
function FeedbackStatusFilter({ onStatusChange, status, }) {
|
|
14
|
+
return (_jsxs(Select, { onValueChange: (value) => updateStatusFilter(value, onStatusChange), value: status, children: [_jsx(SelectTrigger, { "aria-label": "Filter by status", className: "w-full sm:w-40", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: feedbackStatusOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] }));
|
|
15
|
+
}
|
|
16
|
+
function FeedbackTypeFilter({ onTypeChange, type, }) {
|
|
17
|
+
return (_jsxs(Select, { onValueChange: (value) => updateTypeFilter(value, onTypeChange), value: type, children: [_jsx(SelectTrigger, { "aria-label": "Filter by type", className: "w-full sm:w-44", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: feedbackTypeOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] }));
|
|
18
|
+
}
|
|
19
|
+
function ClearFiltersButton({ onClearFilters }) {
|
|
20
|
+
return (_jsxs(Button, { onClick: onClearFilters, type: "button", variant: "outline", children: [_jsx(XIcon, {}), "Clear filters"] }));
|
|
21
|
+
}
|
|
22
|
+
function updateStatusFilter(value, onStatusChange) {
|
|
23
|
+
if (isStatusFilter(value))
|
|
24
|
+
onStatusChange(value);
|
|
25
|
+
}
|
|
26
|
+
function updateTypeFilter(value, onTypeChange) {
|
|
27
|
+
if (isTypeFilter(value))
|
|
28
|
+
onTypeChange(value);
|
|
29
|
+
}
|
|
30
|
+
function isStatusFilter(value) {
|
|
31
|
+
return feedbackStatusOptions.some((option) => option.value === value);
|
|
32
|
+
}
|
|
33
|
+
function isTypeFilter(value) {
|
|
34
|
+
return feedbackTypeOptions.some((option) => option.value === value);
|
|
35
|
+
}
|
|
36
|
+
export { ClearFiltersButton, FeedbackFiltersToolbar, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type FeedbackMetrics, type StakeholderFeedbackItem } from "@ciridae/ui/feedback-tracking-types";
|
|
3
|
+
type FeedbackStakeholderPageProps = {
|
|
4
|
+
isLoading?: boolean;
|
|
5
|
+
items: StakeholderFeedbackItem[];
|
|
6
|
+
metrics: FeedbackMetrics;
|
|
7
|
+
};
|
|
8
|
+
declare function FeedbackStakeholderPage({ isLoading, items, metrics, }: FeedbackStakeholderPageProps): React.JSX.Element;
|
|
9
|
+
export { FeedbackStakeholderPage, type FeedbackStakeholderPageProps };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { FeedbackDetailSheet } from "@ciridae/ui/feedback-detail-sheet";
|
|
4
|
+
import { FeedbackMetricsStrip } from "@ciridae/ui/feedback-metrics";
|
|
5
|
+
import { FeedbackTable } from "@ciridae/ui/feedback-table";
|
|
6
|
+
import { ClearFiltersButton, FeedbackFiltersToolbar, } from "@ciridae/ui/feedback-stakeholder-page-parts";
|
|
7
|
+
import { PageHeader, PageHeaderContent, PageHeaderDescription, PageHeaderMain, PageHeaderTitle, } from "@ciridae/ui/page-header";
|
|
8
|
+
function FeedbackStakeholderPage({ isLoading = false, items, metrics, }) {
|
|
9
|
+
const [filters, setFilters] = React.useState(defaultFilters);
|
|
10
|
+
const [selectedFeedback, setSelectedFeedback] = React.useState();
|
|
11
|
+
const filteredItems = React.useMemo(() => filterFeedbackItems(items, filters), [items, filters]);
|
|
12
|
+
const hasActiveFilters = !areDefaultFilters(filters);
|
|
13
|
+
return (_jsxs("div", { className: "min-h-svh bg-background text-foreground", children: [_jsx(FeedbackStakeholderPageHeader, {}), _jsxs("main", { className: "grid gap-4 px-4 py-4 md:px-6 lg:px-8", children: [_jsx(FeedbackMetricsStrip, { metrics: metrics }), _jsx(FeedbackTrackerSurface, { filters: filters, hasActiveFilters: hasActiveFilters, isLoading: isLoading, items: filteredItems, onClearFilters: () => setFilters(defaultFilters), onFiltersChange: setFilters, onSelectFeedback: setSelectedFeedback })] }), _jsx(FeedbackDetailSheet, { feedback: selectedFeedback, onOpenChange: (open) => {
|
|
14
|
+
if (!open)
|
|
15
|
+
setSelectedFeedback(undefined);
|
|
16
|
+
}, open: Boolean(selectedFeedback) })] }));
|
|
17
|
+
}
|
|
18
|
+
function FeedbackStakeholderPageHeader() {
|
|
19
|
+
return (_jsx(PageHeader, { children: _jsx(PageHeaderMain, { children: _jsxs(PageHeaderContent, { children: [_jsx(PageHeaderTitle, { children: "Feedback" }), _jsx(PageHeaderDescription, { children: "Track submitted feedback and Ciridae's progress on review and implementation." })] }) }) }));
|
|
20
|
+
}
|
|
21
|
+
function FeedbackTrackerSurface({ filters, hasActiveFilters, isLoading, items, onClearFilters, onFiltersChange, onSelectFeedback, }) {
|
|
22
|
+
return (_jsxs("div", { className: "grid gap-(--panel-gap)", children: [_jsx(FeedbackFiltersToolbar, { filters: filters, hasActiveFilters: hasActiveFilters, onClearFilters: onClearFilters, onFiltersChange: onFiltersChange }), _jsx(FeedbackTable, { emptyAction: hasActiveFilters ? _jsx(ClearFiltersButton, { onClearFilters: onClearFilters }) : null, emptyDescription: getEmptyDescription(hasActiveFilters), emptyTitle: hasActiveFilters ? "No matching feedback" : "No feedback", isLoading: isLoading, items: items, onSelectFeedback: onSelectFeedback })] }));
|
|
23
|
+
}
|
|
24
|
+
const defaultFilters = {
|
|
25
|
+
search: "",
|
|
26
|
+
status: "all",
|
|
27
|
+
type: "all",
|
|
28
|
+
};
|
|
29
|
+
function filterFeedbackItems(items, filters) {
|
|
30
|
+
return items.filter((item) => {
|
|
31
|
+
return (matchesSearch(item, filters.search) &&
|
|
32
|
+
matchesStatus(item, filters.status) &&
|
|
33
|
+
matchesType(item, filters.type));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function matchesSearch(item, search) {
|
|
37
|
+
const query = search.trim().toLowerCase();
|
|
38
|
+
if (!query)
|
|
39
|
+
return true;
|
|
40
|
+
return [
|
|
41
|
+
item.subject,
|
|
42
|
+
item.description,
|
|
43
|
+
item.submittedBy.name,
|
|
44
|
+
item.submittedBy.email,
|
|
45
|
+
item.referenceId,
|
|
46
|
+
]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.some((value) => value?.toLowerCase().includes(query));
|
|
49
|
+
}
|
|
50
|
+
function matchesStatus(item, status) {
|
|
51
|
+
return status === "all" || item.status === status;
|
|
52
|
+
}
|
|
53
|
+
function matchesType(item, type) {
|
|
54
|
+
return type === "all" || item.type === type;
|
|
55
|
+
}
|
|
56
|
+
function areDefaultFilters(filters) {
|
|
57
|
+
return (filters.search === defaultFilters.search &&
|
|
58
|
+
filters.status === defaultFilters.status &&
|
|
59
|
+
filters.type === defaultFilters.type);
|
|
60
|
+
}
|
|
61
|
+
function getEmptyDescription(hasActiveFilters) {
|
|
62
|
+
if (hasActiveFilters)
|
|
63
|
+
return "No feedback matches the current filters.";
|
|
64
|
+
return "Feedback submitted by users will appear here.";
|
|
65
|
+
}
|
|
66
|
+
export { FeedbackStakeholderPage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { KanbanCardData, KanbanColumnData, KanbanMoveEvent } from "@ciridae/ui/kanban-types";
|
|
3
|
+
type KanbanPageProps = {
|
|
4
|
+
cards: KanbanCardData[];
|
|
5
|
+
columns: KanbanColumnData[];
|
|
6
|
+
isLoading?: boolean;
|
|
7
|
+
onCardMove?: (event: KanbanMoveEvent) => void;
|
|
8
|
+
onCreateCard?: () => void;
|
|
9
|
+
renderCardActions?: (card: KanbanCardData) => React.ReactNode;
|
|
10
|
+
renderColumnActions?: (column: KanbanColumnData) => React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
declare function KanbanPage({ cards, columns, isLoading, onCardMove, onCreateCard, renderCardActions, renderColumnActions, }: KanbanPageProps): React.JSX.Element;
|
|
13
|
+
export { KanbanPage, type KanbanPageProps };
|