feat(web): add kanban board page with drag-and-drop
- Add kanban board page at /kanban with 5 status columns (To Do, In Progress, Paused, Done, Archived) - Use @hello-pangea/dnd for drag-and-drop between columns - Optimistic status update on drop with API revert on failure - Add updateTask() to tasks API client - Task cards show title, priority badge, and truncated description - Column headers with colored accent borders and task count badges - Responsive: horizontal scroll with min-width 280px per column - Loading (MosaicSpinner), error, and empty states - All design tokens from CSS custom properties Refs #468 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^9.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mosaic/shared": "workspace:*",
|
||||
"@mosaic/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
|
||||
469
apps/web/src/app/(authenticated)/kanban/page.tsx
Normal file
469
apps/web/src/app/(authenticated)/kanban/page.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { ReactElement } from "react";
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import type {
|
||||
DropResult,
|
||||
DroppableProvided,
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||
import { fetchTasks, updateTask } from "@/lib/api/tasks";
|
||||
import { useWorkspaceId } from "@/lib/hooks";
|
||||
import type { Task } from "@mosaic/shared";
|
||||
import { TaskStatus, TaskPriority } from "@mosaic/shared";
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Column configuration
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
interface ColumnConfig {
|
||||
status: TaskStatus;
|
||||
label: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
const COLUMNS: ColumnConfig[] = [
|
||||
{ status: TaskStatus.NOT_STARTED, label: "To Do", accent: "var(--ms-blue-400)" },
|
||||
{ status: TaskStatus.IN_PROGRESS, label: "In Progress", accent: "var(--ms-amber-400)" },
|
||||
{ status: TaskStatus.PAUSED, label: "Paused", accent: "var(--ms-purple-400)" },
|
||||
{ status: TaskStatus.COMPLETED, label: "Done", accent: "var(--ms-teal-400)" },
|
||||
{ status: TaskStatus.ARCHIVED, label: "Archived", accent: "var(--muted)" },
|
||||
];
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Priority badge helper
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
interface PriorityStyle {
|
||||
label: string;
|
||||
bg: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
function getPriorityStyle(priority: TaskPriority): PriorityStyle {
|
||||
switch (priority) {
|
||||
case TaskPriority.HIGH:
|
||||
return { label: "High", bg: "rgba(229,72,77,0.15)", color: "var(--danger)" };
|
||||
case TaskPriority.MEDIUM:
|
||||
return { label: "Medium", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" };
|
||||
case TaskPriority.LOW:
|
||||
return { label: "Low", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
|
||||
default:
|
||||
return { label: String(priority), bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Task Card
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
columnAccent: string;
|
||||
}
|
||||
|
||||
function TaskCard({ task, provided, snapshot, columnAccent }: TaskCardProps): ReactElement {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const priorityStyle = getPriorityStyle(task.priority);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
}}
|
||||
style={{
|
||||
background: "var(--surface)",
|
||||
border: `1px solid ${hovered || snapshot.isDragging ? columnAccent : "var(--border)"}`,
|
||||
borderRadius: "var(--r)",
|
||||
padding: 12,
|
||||
marginBottom: 8,
|
||||
cursor: "grab",
|
||||
transition: "border-color 0.15s, box-shadow 0.15s",
|
||||
boxShadow: snapshot.isDragging ? "var(--shadow-lg)" : "none",
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
>
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: "var(--text)",
|
||||
fontSize: "0.875rem",
|
||||
marginBottom: 6,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{task.title}
|
||||
</div>
|
||||
|
||||
{/* Priority badge */}
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "1px 8px",
|
||||
borderRadius: "var(--r-sm)",
|
||||
background: priorityStyle.bg,
|
||||
color: priorityStyle.color,
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 500,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{priorityStyle.label}
|
||||
</span>
|
||||
|
||||
{/* Description */}
|
||||
{task.description && (
|
||||
<p
|
||||
style={{
|
||||
color: "var(--muted)",
|
||||
fontSize: "0.8rem",
|
||||
margin: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: "vertical",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Kanban Column
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
interface KanbanColumnProps {
|
||||
config: ColumnConfig;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
function KanbanColumn({ config, tasks }: KanbanColumnProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minWidth: 280,
|
||||
maxWidth: 340,
|
||||
flex: "1 0 280px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "var(--bg-mid)",
|
||||
borderRadius: "var(--r-lg)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Column header */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: `3px solid ${config.accent}`,
|
||||
padding: "12px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.85rem",
|
||||
color: "var(--text)",
|
||||
}}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
padding: "0 6px",
|
||||
borderRadius: "var(--r)",
|
||||
background: `color-mix(in srgb, ${config.accent} 15%, transparent)`,
|
||||
color: config.accent,
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
fontFamily: "var(--mono)",
|
||||
}}
|
||||
>
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Droppable area */}
|
||||
<Droppable droppableId={config.status}>
|
||||
{(provided: DroppableProvided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
style={{
|
||||
padding: "8px 12px 12px",
|
||||
flex: 1,
|
||||
minHeight: 80,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{tasks.map((task, index) => (
|
||||
<Draggable key={task.id} draggableId={task.id} index={index}>
|
||||
{(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => (
|
||||
<TaskCard
|
||||
task={task}
|
||||
provided={dragProvided}
|
||||
snapshot={dragSnapshot}
|
||||
columnAccent={config.accent}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Kanban Board Page
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
export default function KanbanPage(): ReactElement {
|
||||
const workspaceId = useWorkspaceId();
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/* --- data fetching --- */
|
||||
|
||||
const loadTasks = useCallback(async (wsId: string | null): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const filters = wsId !== null ? { workspaceId: wsId } : {};
|
||||
const data = await fetchTasks(filters);
|
||||
setTasks(data);
|
||||
} catch (err: unknown) {
|
||||
console.error("[Kanban] Failed to fetch tasks:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Something went wrong loading tasks. You could try again when ready."
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceId) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load(): Promise<void> {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const filters = workspaceId !== null ? { workspaceId } : {};
|
||||
const data = await fetchTasks(filters);
|
||||
if (!cancelled) {
|
||||
setTasks(data);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error("[Kanban] Failed to fetch tasks:", err);
|
||||
if (!cancelled) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Something went wrong loading tasks. You could try again when ready."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void load();
|
||||
|
||||
return (): void => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [workspaceId]);
|
||||
|
||||
/* --- group tasks by status --- */
|
||||
|
||||
function groupByStatus(allTasks: Task[]): Record<TaskStatus, Task[]> {
|
||||
const grouped: Record<TaskStatus, Task[]> = {
|
||||
[TaskStatus.NOT_STARTED]: [],
|
||||
[TaskStatus.IN_PROGRESS]: [],
|
||||
[TaskStatus.PAUSED]: [],
|
||||
[TaskStatus.COMPLETED]: [],
|
||||
[TaskStatus.ARCHIVED]: [],
|
||||
};
|
||||
|
||||
for (const task of allTasks) {
|
||||
grouped[task.status].push(task);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
const grouped = groupByStatus(tasks);
|
||||
|
||||
/* --- drag-and-drop handler --- */
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
// Dropped outside a droppable area
|
||||
if (!destination) return;
|
||||
|
||||
// Dropped in same position
|
||||
if (source.droppableId === destination.droppableId && source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatus = destination.droppableId as TaskStatus;
|
||||
const taskId = draggableId;
|
||||
|
||||
// Optimistic update: move card in local state
|
||||
setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t)));
|
||||
|
||||
// Persist to API
|
||||
const wsId = workspaceId ?? undefined;
|
||||
updateTask(taskId, { status: newStatus }, wsId).catch((err: unknown) => {
|
||||
console.error("[Kanban] Failed to update task status:", err);
|
||||
// Revert on failure by re-fetching
|
||||
void loadTasks(workspaceId);
|
||||
});
|
||||
},
|
||||
[workspaceId, loadTasks]
|
||||
);
|
||||
|
||||
/* --- retry handler --- */
|
||||
|
||||
function handleRetry(): void {
|
||||
void loadTasks(workspaceId);
|
||||
}
|
||||
|
||||
/* --- render --- */
|
||||
|
||||
return (
|
||||
<main style={{ padding: "32px 24px", minHeight: "100%" }}>
|
||||
{/* Page header */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "1.875rem",
|
||||
fontWeight: 700,
|
||||
color: "var(--text)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Kanban Board
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
color: "var(--muted)",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
Visualize and manage task progress across stages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<MosaicSpinner label="Loading tasks..." />
|
||||
</div>
|
||||
) : error !== null ? (
|
||||
/* Error state */
|
||||
<div
|
||||
style={{
|
||||
background: "var(--surface)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "var(--r-lg)",
|
||||
padding: 32,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p style={{ color: "var(--danger)", margin: "0 0 16px" }}>{error}</p>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
background: "var(--danger)",
|
||||
border: "none",
|
||||
borderRadius: "var(--r)",
|
||||
color: "#fff",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
/* Empty state */
|
||||
<div
|
||||
style={{
|
||||
background: "var(--surface)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "var(--r-lg)",
|
||||
padding: 48,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p style={{ color: "var(--muted)", margin: 0, fontSize: "0.9rem" }}>
|
||||
No tasks yet. Create some tasks to see them here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Board */
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 16,
|
||||
overflowX: "auto",
|
||||
paddingBottom: 16,
|
||||
minHeight: 400,
|
||||
}}
|
||||
>
|
||||
{COLUMNS.map((col) => (
|
||||
<KanbanColumn key={col.status} config={col} tasks={grouped[col.status]} />
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Task } from "@mosaic/shared";
|
||||
import type { TaskStatus, TaskPriority } from "@mosaic/shared";
|
||||
import { apiGet, type ApiResponse } from "./client";
|
||||
import { apiGet, apiPatch, type ApiResponse } from "./client";
|
||||
|
||||
export interface TaskFilters {
|
||||
status?: TaskStatus;
|
||||
@@ -34,3 +34,15 @@ export async function fetchTasks(filters?: TaskFilters): Promise<Task[]> {
|
||||
const response = await apiGet<ApiResponse<Task[]>>(endpoint, filters?.workspaceId);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task by ID
|
||||
*/
|
||||
export async function updateTask(
|
||||
id: string,
|
||||
data: Partial<Task>,
|
||||
workspaceId?: string
|
||||
): Promise<Task> {
|
||||
const res = await apiPatch<ApiResponse<Task>>(`/api/tasks/${id}`, data, workspaceId);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
35
docs/scratchpads/468-kanban-page.md
Normal file
35
docs/scratchpads/468-kanban-page.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# PG-PAGE-003: Kanban Board Page
|
||||
|
||||
## Task
|
||||
|
||||
Build Kanban board page with drag-and-drop columns mapped to TaskStatus.
|
||||
|
||||
## Issue
|
||||
|
||||
Refs #468
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `apps/web/src/app/(authenticated)/kanban/page.tsx` (new)
|
||||
- `apps/web/src/lib/api/tasks.ts` (added `updateTask`)
|
||||
- `package.json` / `pnpm-lock.yaml` (added `@hello-pangea/dnd`)
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- Used `@hello-pangea/dnd` (maintained fork of react-beautiful-dnd) for drag-and-drop
|
||||
- 5 columns mapped to TaskStatus enum: NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED, ARCHIVED
|
||||
- Optimistic update pattern: move card immediately, revert by re-fetching on API failure
|
||||
- Priority badge always shown (field is non-optional in Task type)
|
||||
- Column count badge uses `color-mix()` for transparent accent backgrounds
|
||||
- Horizontal scroll on mobile with `overflow-x: auto` and `min-width: 280px` per column
|
||||
|
||||
## Verification
|
||||
|
||||
- Lint: clean (my files pass, pre-existing errors in runner-jobs.ts not my scope)
|
||||
- Build: `next build` succeeds, `/kanban` route present in output
|
||||
- TypeScript: no type errors
|
||||
- Design tokens: all colors from CSS custom properties, no hardcoded colors
|
||||
|
||||
## Status
|
||||
|
||||
Complete
|
||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@@ -386,6 +386,9 @@ importers:
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.2.4)
|
||||
'@hello-pangea/dnd':
|
||||
specifier: ^18.0.1
|
||||
version: 18.0.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@mosaic/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared
|
||||
@@ -1172,6 +1175,12 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
'@hello-pangea/dnd@18.0.1':
|
||||
resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -1529,7 +1538,6 @@ packages:
|
||||
|
||||
'@mosaicstack/telemetry-client@0.1.1':
|
||||
resolution: {integrity: sha512-1udg6p4cs8rhQgQ2pKCfi7EpRlJieRRhA5CIqthRQ6HQZLgQ0wH+632jEulov3rlHSM1iplIQ+AAe5DWrvSkEA==, tarball: https://git.mosaicstack.dev/api/packages/mosaic/npm/%40mosaicstack%2Ftelemetry-client/-/0.1.1/telemetry-client-0.1.1.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@mrleebo/prisma-ast@0.13.1':
|
||||
resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==}
|
||||
@@ -3944,6 +3952,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-box-model@1.2.1:
|
||||
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
||||
|
||||
css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
@@ -5996,6 +6007,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
raf-schd@4.0.3:
|
||||
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@@ -7559,7 +7573,7 @@ snapshots:
|
||||
chalk: 5.6.2
|
||||
commander: 12.1.0
|
||||
dotenv: 17.2.4
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
open: 10.2.0
|
||||
pg: 8.17.2
|
||||
prettier: 3.8.1
|
||||
@@ -7951,6 +7965,18 @@ snapshots:
|
||||
protobufjs: 7.5.4
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hello-pangea/dnd@18.0.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
css-box-model: 1.2.1
|
||||
raf-schd: 4.0.3
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
react-redux: 9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1)
|
||||
redux: 5.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@@ -10621,7 +10647,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
better-sqlite3: 12.6.2
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
pg: 8.17.2
|
||||
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
|
||||
@@ -10646,7 +10672,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3)
|
||||
better-sqlite3: 12.6.2
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
pg: 8.17.2
|
||||
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
|
||||
@@ -11099,6 +11125,10 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-box-model@1.2.1:
|
||||
dependencies:
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
@@ -11455,6 +11485,17 @@ snapshots:
|
||||
|
||||
dotenv@17.2.4: {}
|
||||
|
||||
drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
|
||||
'@types/pg': 8.16.0
|
||||
better-sqlite3: 12.6.2
|
||||
kysely: 0.28.10
|
||||
pg: 8.17.2
|
||||
postgres: 3.4.8
|
||||
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
|
||||
|
||||
drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -11465,6 +11506,7 @@ snapshots:
|
||||
pg: 8.17.2
|
||||
postgres: 3.4.8
|
||||
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
|
||||
optional: true
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
@@ -13167,6 +13209,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
raf-schd@4.0.3: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
Reference in New Issue
Block a user