From db271a6546efcdd9360137d136f8752d7c4e2699 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 15 Jul 2026 06:47:31 -0300 Subject: [PATCH] small ui quirks --- project-context.md | 4 +- web/src/components/BoardCanvas.tsx | 36 -------------- web/src/components/CardDetailModal.tsx | 7 ++- web/src/styles.css | 69 +++----------------------- 4 files changed, 11 insertions(+), 105 deletions(-) diff --git a/project-context.md b/project-context.md index 5a0405f..9a0e78c 100644 --- a/project-context.md +++ b/project-context.md @@ -42,7 +42,7 @@ Havenllo (Haven + Trello) is a lightweight, single-user, no-authentication kanba - `web/src/markdown.ts`: safe CommonMark renderer using `commonmark` plus DOMPurify, and plain-text extraction for card summaries. - `web/src/App.tsx`: board loading, optimistic mutations, selected-card state, and component wiring. - `web/src/components/Header.tsx`: Havenllo operations-board brand, local-ready state, refresh, and list-management toggle. -- `web/src/components/BoardCanvas.tsx`: control-room board intro, live/edit badge, derived summary metrics, command-deck guidance, centered-or-scrollable rail, list-management add control, and board-level Pointer Events drag lifecycle. It detects whole-column targets, insertion indices, pointer cancellation, and Escape cancellation. +- `web/src/components/BoardCanvas.tsx`: centered-or-scrollable workflow rail, list-management add control, task count, and board-level Pointer Events drag lifecycle. It detects whole-column targets, insertion indices, pointer cancellation, and Escape cancellation. - `web/src/components/ListColumn.tsx`: stage marker/kicker, column header/count, drop target and insertion marker, inline card creation, and management-only rename/delete controls. - `web/src/components/CardItem.tsx`: focusable card tile with a dedicated drag grip, current-list status chip, plain-text Markdown excerpt, and open-card affordance. Clicking outside the grip opens details. - `web/src/components/CardDetailModal.tsx`: accessible card dialog with title editing, current-list status, native move-to-list selector, CommonMark Write/Preview description editor, timestamps, and delete confirmation. @@ -106,7 +106,7 @@ Errors use `{ "error": { "code", "message" } }`. Success uses `200`, `201`, or ` - Start a pointer drag from a card's grip. The entire destination column accepts it; the nearest card midpoint controls the insertion marker and fractional position. Mouse and touch both work. Pointer cancel or Escape aborts without a request. Drops update optimistically and roll back with a toast on failure. - The card dialog also has a native Move to list select, which appends the card through the existing `PATCH /api/cards/{id}` behavior and provides a keyboard-friendly alternative. - Status is derived solely from the card's current list. Tiles show the list-name chip; the dialog shows the same read-only status. `Done` has no special backend meaning. -- Descriptions remain raw strings in the API/database. The dialog writes CommonMark source and previews safe, sanitized CommonMark; card tiles show a compact plain-text rendering. +- Descriptions remain raw strings in the API/database. The dialog opens in a safe, sanitized CommonMark Preview tab and switches to Write mode for editing; card tiles show a compact plain-text rendering. - The Edit toggle exposes list add/rename/delete controls. Card add, edit, delete, movement, and detail viewing remain available outside list-management mode. - Short rails are horizontally centered on wide screens. Overflowing rails begin at the first column and use responsive horizontal scrolling and mobile scroll snapping. diff --git a/web/src/components/BoardCanvas.tsx b/web/src/components/BoardCanvas.tsx index c956882..2a457c1 100644 --- a/web/src/components/BoardCanvas.tsx +++ b/web/src/components/BoardCanvas.tsx @@ -44,8 +44,6 @@ export function BoardCanvas(props: BoardCanvasProps) { const dragRef = useRef(null); const cleanupDragRef = useRef<() => void>(() => undefined); const totalCards = props.lists.reduce((total, list) => total + list.cards.length, 0); - const activeCards = props.lists[0]?.cards.length ?? 0; - const filledLists = props.lists.filter((list) => list.cards.length > 0).length; useEffect(() => () => cleanupDragRef.current(), []); @@ -135,40 +133,6 @@ export function BoardCanvas(props: BoardCanvasProps) { return (
-
-
-
-

Workspace / Haven homelab

- {props.managingLists ? "Edit mode" : "Live board"} -
-

Haven control room

-

A clear runway for the work that keeps your little corner of the internet humming.

-
-
- Command deck -

- {props.managingLists ? "Shape the workflow: rename, remove, or add stages." : "Grab a handle to move a task, or open one for the full story."} -

- SPACE to open a card -
-
-
-
- Tasks in orbit - {totalCards} - across the board -
-
- Workflow stages - {props.lists.length} - {filledLists} with activity -
-
- Front of queue - {activeCards} - in the first stage -
-
{props.lists.length ? (
Workflow rail{totalCards} {totalCards === 1 ? "task" : "tasks"} in flight
diff --git a/web/src/components/CardDetailModal.tsx b/web/src/components/CardDetailModal.tsx index 2ea172d..b5d94b0 100644 --- a/web/src/components/CardDetailModal.tsx +++ b/web/src/components/CardDetailModal.tsx @@ -24,7 +24,7 @@ function formatTimestamp(value: string): string { export function CardDetailModal({ card, listName, lists, onClose, onEdit, onDelete }: CardDetailModalProps) { const [title, setTitle] = useState(card.title); const [description, setDescription] = useState(card.description); - const [descriptionMode, setDescriptionMode] = useState<"write" | "preview">("write"); + const [descriptionMode, setDescriptionMode] = useState<"write" | "preview">("preview"); const [savingTitle, setSavingTitle] = useState(false); const [savingDescription, setSavingDescription] = useState(false); const [savingList, setSavingList] = useState(false); @@ -41,7 +41,7 @@ export function CardDetailModal({ card, listName, lists, onClose, onEdit, onDele useEffect(() => { setTitle(card.title); setDescription(card.description); - setDescriptionMode("write"); + setDescriptionMode("preview"); }, [card.id]); useEffect(() => { @@ -195,11 +195,10 @@ export function CardDetailModal({ card, listName, lists, onClose, onEdit, onDele
-
+

Details

-

Description

{descriptionChanged ? Unsaved changes : null}
diff --git a/web/src/styles.css b/web/src/styles.css index 730d436..5225350 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -157,24 +157,6 @@ button:disabled { cursor: not-allowed; opacity: 0.55; } .board-area { padding: clamp(26px, 4.2vw, 58px) clamp(18px, 4vw, 56px) 34px; } -.board-intro { - max-width: 1530px; - margin: 0 auto 26px; - display: flex; - align-items: end; - justify-content: space-between; - gap: 20px; -} - -.board-intro h1 { - margin: 5px 0 0; - font-size: clamp(1.72rem, 3vw, 2.45rem); - letter-spacing: -0.055em; - line-height: 1; -} - -.board-hint { max-width: 335px; margin: 0 2px 2px 0; color: var(--muted); font-size: 0.89rem; line-height: 1.5; text-align: right; } - .board-rail { width: min(100%, 1530px); min-height: 320px; @@ -497,24 +479,23 @@ textarea { min-height: 96px; resize: vertical; } .detail-title-input:focus { padding-right: 8px; padding-left: 8px; border-color: var(--teal); background: rgba(7, 14, 27, 0.48); } .detail-properties { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + display: flex; + align-items: stretch; + flex-wrap: nowrap; gap: 10px; margin: 18px 0 26px 27px; } -.detail-property { min-height: 66px; display: grid; align-content: center; gap: 5px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px; background: rgba(5, 12, 26, 0.22); } +.detail-property { min-width: 0; min-height: 66px; flex: 1 1 0; display: grid; align-content: center; gap: 5px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px; background: rgba(5, 12, 26, 0.22); } .detail-property > span { color: var(--faint); font-size: 0.69rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } .detail-property strong { overflow: hidden; color: var(--ink); font-size: 0.87rem; text-overflow: ellipsis; white-space: nowrap; } .detail-status { display: flex; align-content: initial; align-items: center; justify-content: space-between; gap: 8px; } .detail-status .status-pill { max-width: 190px; color: var(--teal); border-color: rgba(101, 218, 203, 0.42); background: var(--teal-soft); } -.detail-move { grid-column: 1 / -1; } .detail-move select { min-height: 38px; padding-top: 7px; padding-bottom: 7px; font-size: 0.84rem; } .detail-description { margin-left: 27px; } .detail-section-heading { justify-content: space-between; gap: 12px; margin-bottom: 10px; } -.detail-section-heading h2 { margin: 3px 0 0; color: var(--ink); font-size: 1.03rem; letter-spacing: -0.02em; } .unsaved-chip { padding: 4px 8px; border: 1px solid rgba(145, 164, 255, 0.34); border-radius: 999px; color: #cbd4ff; font-size: 0.69rem; font-weight: 720; background: rgba(145, 164, 255, 0.11); } .detail-description textarea { min-height: 150px; } @@ -608,8 +589,6 @@ textarea { min-height: 96px; resize: vertical; } .topbar-board { display: none; } .header-edit-button { min-width: 44px; padding: 8px 10px; } .board-area { padding: 25px 16px 26px; overflow: hidden; } - .board-intro { align-items: flex-start; margin-bottom: 19px; } - .board-hint { display: none; } .board-rail { width: calc(100% + 16px); margin-right: -16px; padding-right: 16px; scroll-snap-type: x proximity; } .board-list-group { justify-content: flex-start; } .list-column, .add-list-column { scroll-snap-align: start; } @@ -617,7 +596,7 @@ textarea { min-height: 96px; resize: vertical; } .detail-backdrop, .dialog-backdrop { padding: 10px; } .detail-header { padding: 13px 13px 0; } .detail-content { padding: 6px 18px 20px; } - .detail-properties { grid-template-columns: 1fr; margin-left: 0; } + .detail-properties { margin-left: 0; } .detail-description, .detail-footer { margin-left: 0; } .detail-footer { align-items: flex-start; flex-direction: column; gap: 12px; } .detail-delete { padding-left: 0; } @@ -723,7 +702,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus- .topbar-board { gap: 3px; padding-left: 22px; border-left-color: rgba(198, 216, 203, 0.2); } .topbar-board strong { display: flex; align-items: center; gap: 8px; font-size: 0.88rem; } .local-state { display: inline-flex; align-items: center; gap: 5px; color: var(--faint); font-size: 0.63rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; } -.local-state i, .status-dot, .board-live-badge i { width: 6px; height: 6px; display: inline-block; border-radius: 50%; background: var(--signal); box-shadow: 0 0 0 4px rgba(215, 255, 99, 0.1), 0 0 13px rgba(215, 255, 99, 0.9); } +.local-state i, .status-dot { width: 6px; height: 6px; display: inline-block; border-radius: 50%; background: var(--signal); box-shadow: 0 0 0 4px rgba(215, 255, 99, 0.1), 0 0 13px rgba(215, 255, 99, 0.9); } .topbar-signal { display: inline-flex; align-items: center; gap: 9px; margin-left: auto; color: var(--faint); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; } .topbar-signal .status-dot { width: 7px; height: 7px; } .topbar-actions { margin-left: 2px; } @@ -737,30 +716,6 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus- .icon-button:hover:not(:disabled) { color: var(--signal); border-color: rgba(215, 255, 99, 0.42); background: var(--signal-soft); transform: translateY(-1px); } .board-area { max-width: 1660px; padding: 56px clamp(20px, 4.5vw, 72px) 76px; } -.board-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(245px, 335px); align-items: end; gap: 36px; margin-bottom: 24px; } -.board-title-block { min-width: 0; } -.eyebrow-row { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; } -.board-live-badge { display: inline-flex; align-items: center; gap: 7px; min-height: 25px; padding: 4px 9px; border: 1px solid rgba(215, 255, 99, 0.23); border-radius: 999px; color: var(--signal); font-size: 0.6rem; font-weight: 850; letter-spacing: 0.11em; text-transform: uppercase; background: rgba(215, 255, 99, 0.06); } -.board-live-badge i { width: 5px; height: 5px; } -.board-intro h1 { margin: 0; color: var(--ink); font-size: clamp(2.5rem, 5.3vw, 5.35rem); font-weight: 850; letter-spacing: -0.075em; line-height: 0.94; } -.board-intro h1 span { color: var(--muted); font-weight: 450; } -.board-description { max-width: 590px; margin: 20px 0 0; color: var(--muted); font-size: 1rem; line-height: 1.55; } -.board-command-card { position: relative; padding: 17px 18px 16px; border: 1px solid rgba(198, 216, 203, 0.15); border-radius: 16px 16px 4px 16px; background: linear-gradient(145deg, rgba(23, 30, 40, 0.94), rgba(12, 16, 23, 0.87)); box-shadow: var(--shadow); } -.board-command-card::before { position: absolute; top: 0; right: 20px; left: 20px; height: 1px; background: linear-gradient(90deg, transparent, var(--signal), transparent); content: ""; opacity: 0.8; } -.command-card-label { display: block; color: var(--signal); font-size: 0.61rem; font-weight: 850; letter-spacing: 0.16em; text-transform: uppercase; } -.board-hint { max-width: none; margin: 10px 0 14px; color: var(--ink); font-size: 0.82rem; line-height: 1.5; text-align: left; } -.command-card-key { color: var(--faint); font-size: 0.66rem; } -kbd { padding: 3px 5px; border: 1px solid rgba(198, 216, 203, 0.18); border-bottom-color: rgba(198, 216, 203, 0.35); border-radius: 4px; color: var(--muted); font-size: 0.58rem; font-weight: 850; letter-spacing: 0.08em; background: rgba(0, 0, 0, 0.25); } - -.board-summary { display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 10px; margin-bottom: 38px; } -.board-metric { position: relative; min-height: 93px; display: grid; align-content: center; gap: 3px; padding: 14px 17px; overflow: hidden; border: 1px solid rgba(198, 216, 203, 0.12); border-radius: 15px; background: rgba(16, 21, 29, 0.63); } -.board-metric::after { position: absolute; right: -18px; bottom: -32px; width: 95px; height: 95px; border: 1px solid rgba(198, 216, 203, 0.08); border-radius: 50%; content: ""; } -.board-metric-primary { border-color: rgba(215, 255, 99, 0.28); background: linear-gradient(135deg, rgba(215, 255, 99, 0.11), rgba(16, 21, 29, 0.66) 56%); } -.metric-label { color: var(--muted); font-size: 0.63rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; } -.board-metric strong { color: var(--ink); font-size: 2rem; font-weight: 760; letter-spacing: -0.06em; line-height: 1; } -.board-metric-primary strong { color: var(--signal); } -.metric-note { color: var(--faint); font-size: 0.66rem; } - .board-rail { width: min(100%, 1560px); min-height: 320px; padding: 0 2px 34px; scrollbar-color: rgba(215, 255, 99, 0.3) transparent; } .rail-header { display: flex; align-items: center; gap: 11px; margin: 0 2px 13px; color: var(--faint); font-size: 0.62rem; font-weight: 850; letter-spacing: 0.14em; text-transform: uppercase; } .rail-header i { height: 1px; flex: 1; background: linear-gradient(90deg, rgba(215, 255, 99, 0.34), transparent); } @@ -854,7 +809,6 @@ input:focus, textarea:focus, select:focus { border-color: var(--signal); backgro .detail-property > span { color: var(--faint); font-size: 0.61rem; letter-spacing: 0.13em; } .detail-status .status-pill { color: var(--signal); border-color: rgba(215, 255, 99, 0.34); background: var(--signal-soft); } .detail-description { margin-top: 4px; } -.detail-section-heading h2 { font-size: 1.12rem; } .markdown-tabs button.is-active { color: var(--signal); border-color: rgba(215, 255, 99, 0.34); background: var(--signal-soft); } .detail-description textarea, .markdown-preview { border-color: rgba(198, 216, 203, 0.2); border-radius: 13px; background: rgba(5, 8, 13, 0.45); } .markdown-preview blockquote { border-left-color: var(--signal); } @@ -868,8 +822,6 @@ input:focus, textarea:focus, select:focus { border-color: var(--signal); backgro @media (max-width: 850px) { .topbar-signal { display: none; } - .board-intro { grid-template-columns: minmax(0, 1fr) minmax(220px, 0.65fr); gap: 22px; } - .board-intro h1 { font-size: clamp(2.55rem, 7vw, 4.4rem); } } @media (max-width: 700px) { @@ -877,14 +829,6 @@ input:focus, textarea:focus, select:focus { border-color: var(--signal); backgro .topbar-board { display: none; } .topbar-actions { gap: 6px; } .board-area { padding: 38px 16px 48px; } - .board-intro { display: block; margin-bottom: 20px; } - .board-intro h1 { font-size: clamp(2.75rem, 13vw, 4.5rem); } - .board-description { margin-top: 15px; font-size: 0.9rem; } - .board-command-card { margin-top: 24px; } - .board-summary { grid-template-columns: repeat(3, minmax(126px, 1fr)); gap: 8px; margin: 0 -2px 28px; overflow-x: auto; } - .board-metric { min-height: 88px; padding: 12px; } - .board-metric strong { font-size: 1.7rem; } - .metric-label { font-size: 0.56rem; } .board-rail { width: calc(100% + 16px); margin-right: -16px; padding-right: 16px; } .rail-header { margin-right: 16px; } .board-list-group { justify-content: flex-start; } @@ -898,7 +842,6 @@ input:focus, textarea:focus, select:focus { border-color: var(--signal); backgro .brand-wordmark small { display: none; } .header-edit-button { min-width: 40px; padding: 8px 10px; font-size: 0; } .header-edit-button span { font-size: 1rem; } - .board-summary { margin-right: -16px; padding-right: 16px; } .list-column, .add-list-column { width: calc(100vw - 48px); flex-basis: calc(100vw - 48px); } .detail-timestamps { display: grid; gap: 9px; } }