feat: card checklists (#141)

* feat: setup checklist schema

* feat: scaffold checklist router

* feat: add create new checklist modal

* feat: create new checklist item

* feat: toggle checklist item completed state

* feat: update checklist name

* feat: delete checklist

* feat: show checklists progress on cards

* feat: tweak light mode styling

* feat: focus item form on creation of new checklist

* feat: tweak new checklist item form styling

* feat: add checklist activity

* feat: show checklist progress on public board page

* feat: display checklist items on public card modal

* chore: add translations
This commit is contained in:
Henry
2025-08-14 19:59:36 +01:00
committed by GitHub
parent 80d1c5f805
commit 13b10471ec
45 changed files with 8171 additions and 684 deletions

View File

@@ -5,7 +5,7 @@ import LoadingSpinner from "./LoadingSpinner";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "sm" | "md" | "lg";
size?: "xs" | "sm" | "md" | "lg";
isLoading?: boolean;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
@@ -28,6 +28,7 @@ const Button = ({
}: ButtonProps) => {
const classes = twMerge(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none",
size === "xs" && "text-xs px-2 py-1",
size === "sm" && "text-xs",
size === "lg" && "py-[0.65rem]",
fullWidth && "w-full",

View File

@@ -0,0 +1,58 @@
import { twMerge } from "tailwind-merge";
interface CircularProgressProps {
progress: number; // 0-100
size?: "sm" | "md" | "lg";
className?: string;
}
const CircularProgress = ({
progress,
size = "md",
className,
}: CircularProgressProps) => {
const radius = 40;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (progress / 100) * circumference;
return (
<div className={twMerge("relative", className)}>
<svg
className={twMerge(
"-rotate-90 transform",
size === "sm" && "h-4 w-4",
size === "md" && "h-5 w-5",
size === "lg" && "h-8 w-8",
)}
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="14"
className="text-light-300 dark:text-dark-400"
/>
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="14"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className={twMerge(
"transition-all duration-300 ease-in-out",
progress === 100 ? "text-green-500" : "text-blue-500",
)}
/>
</svg>
</div>
);
};
export default CircularProgress;

View File

@@ -15,10 +15,15 @@ import {
ReactRenderer,
useEditor,
} from "@tiptap/react";
import { useEffect } from "react";
import StarterKit from "@tiptap/starter-kit";
import Suggestion from "@tiptap/suggestion";
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import {
HiH1,
HiH2,
@@ -547,7 +552,7 @@ export default function Editor({
{!readOnly && editor && <EditorBubbleMenu editor={editor} />}
<EditorContent
editor={editor}
className="prose dark:prose-invert prose-sm max-w-none overflow-y-auto [&_blockquote]:!text-xs [&_h1]:!text-lg [&_h2]:!text-base [&_h3]:!text-sm [&_ol]:!text-xs [&_p.is-empty::before]:text-light-900 [&_p.is-empty::before]:dark:text-dark-800 [&_p]:!text-sm [&_p]:text-black [&_p]:dark:text-white [&_ul]:!text-xs"
className="prose dark:prose-invert prose-sm max-w-none overflow-y-auto [&_blockquote]:!text-xs [&_h1]:!text-lg [&_h2]:!text-base [&_h3]:!text-sm [&_ol]:!text-xs [&_p.is-empty::before]:text-light-900 [&_p.is-empty::before]:dark:text-dark-800 [&_p]:!text-sm [&_p]:text-light-950 [&_p]:dark:text-dark-950 [&_ul]:!text-xs"
/>
</div>
);