- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.1.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import { Fragment, useEffect, useRef, useState } from "react";
import { COLUMN_ACCENT, type Card, type Column } from "../model";
import { CardView, type CardHandlers, type GhostInfo } from "./CardView";
import { IconPlus } from "./Icons";
export type PlaceholderInfo = { colId: string; before: string | "end"; height: number } | null;
type Props = {
column: Column;
cards: Card[];
totalCards: number;
filtered: boolean;
ghostCardId: string | null;
ghostStyle: GhostInfo;
liftedCardId: string | null;
placeholder: PlaceholderInfo;
cardHandlers: CardHandlers;
onCreate: (colId: string, title: string) => string | null;
};
export function ColumnView({
column,
cards,
totalCards,
filtered,
ghostCardId,
ghostStyle,
liftedCardId,
placeholder,
cardHandlers,
onCreate,
}: Props) {
const [composing, setComposing] = useState(false);
const [draft, setDraft] = useState("");
const [error, setError] = useState<string | null>(null);
const taRef = useRef<HTMLTextAreaElement>(null);
const addBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (composing) taRef.current?.focus();
}, [composing]);
const here = placeholder != null && placeholder.colId === column.id ? placeholder : null;
const full = column.wip_limit != null && totalCards >= column.wip_limit;
const accent = COLUMN_ACCENT[column.id] ?? "#8A8F98";
function openComposer() {
setComposing(true);
setDraft("");
setError(null);
}
function closeComposer() {
setComposing(false);
setDraft("");
setError(null);
}
function submit() {
const title = draft.trim();
if (!title) {
setError("A card needs a title.");
return;
}
const err = onCreate(column.id, title);
if (err) {
setError(err);
return;
}
setComposing(false);
setDraft("");
setError(null);
requestAnimationFrame(() => addBtnRef.current?.focus());
}
return (
<section className="column" data-col-id={column.id} aria-labelledby={`col-${column.id}`}>
<h2 className="col-title" id={`col-${column.id}`} tabIndex={-1}>
<span className="col-dot" style={{ background: accent }} aria-hidden="true" />
{column.title}
<span className="count-chip">{filtered ? `${cards.length} of ${totalCards}` : cards.length}</span>
{column.wip_limit != null && (
<span className={`wip-chip${full ? " full" : ""}`} title={`Work in progress limit: ${column.wip_limit} cards`}>
WIP {column.wip_limit}
</span>
)}
</h2>
<div className="col-list" data-col-list>
{cards.length === 0 && here == null && (
<p className="col-empty">{filtered ? "No cards match the filters here." : "No cards yet — add the first one."}</p>
)}
{cards.map(card => (
<Fragment key={card.id}>
{here != null && here.before === card.id && <div className="drop-placeholder" style={{ height: here.height }} />}
<CardView
card={card}
colId={column.id}
ghost={ghostCardId === card.id}
ghostStyle={ghostStyle}
lifted={liftedCardId === card.id}
handlers={cardHandlers}
/>
</Fragment>
))}
{here != null && here.before === "end" && <div className="drop-placeholder" style={{ height: here.height }} />}
</div>
{composing ? (
<div
className="composer"
onKeyDown={e => {
if (e.key === "Escape") {
e.stopPropagation();
closeComposer();
requestAnimationFrame(() => addBtnRef.current?.focus());
}
}}
>
<label className="sr-only" htmlFor={`composer-${column.id}`}>
Card title
</label>
<textarea
id={`composer-${column.id}`}
ref={taRef}
rows={2}
placeholder="Card title (required)"
value={draft}
onChange={e => {
setDraft(e.target.value);
if (error) setError(null);
}}
onKeyDown={e => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit();
}
}}
/>
{error != null && (
<p className="composer-error" role="alert">
{error}
</p>
)}
<div className="composer-row">
<button type="button" className="btn primary" onClick={submit}>
Add card
</button>
<button type="button" className="btn subtle" onClick={closeComposer}>
Cancel
</button>
</div>
</div>
) : (
<button type="button" className="add-card-btn" ref={addBtnRef} onClick={openComposer}>
<IconPlus size={14} />
Add card
</button>
)}
</section>
);
}