mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-29 08:04:58 +00:00
feat: phase 3 renderers & ergonomics — index/console, ⌘K palette, icon suggest
- Index renderer (editorial): serif masthead + section heads, numbered full-width rows, live stat strip (time/services/up/down/widgets) - Console renderer (terminal): $ dash status banner, bordered stat panels, ## section heads, › prompt rows, [ up ]/[ down ] tags - Renderer contract gains index prop for row/section numbering - Settings: 2x2 renderer radio grid with per-renderer descriptions - ⌘K/Ctrl+K// palette: actions + service endpoints, arrow-key nav, zero-dep implementation on the existing Dialog primitive - Icon picker: dashboard-icons slug suggestion chip + live preview - Fonts: Instrument Serif, JetBrains Mono (fontsource) - Fix: global :focus-visible rule was unlayered and silently overrode all focus-visible: utilities since phase 1 — moved into @layer base Verified: typecheck/lint/build clean, dnd + collapse + popover in both new renderers, palette filter + execute, icon suggest resolves real CDN assets, 360px + light theme, docker image 43.9 MB serving board + widget types. Generated with [Devin](https://devin.ai) Authored By: TDvorak <[email protected]> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
8afc7df53b
commit
55fb57456e
16 files changed
+803
-51
No files matched your search
@@ -0,0 +1,224 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChevronDown, GripVertical, Pencil, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useItemDnd, useSectionDnd } from "@/board/dnd";
|
||||
import { useBoard } from "@/board/store";
|
||||
import { IconImg, ItemAnchor, hostOf } from "@/board/primitives";
|
||||
import { WidgetContent } from "@/widgets";
|
||||
import type {
|
||||
BoardViewProps,
|
||||
ItemViewProps,
|
||||
RendererViews,
|
||||
SectionViewProps,
|
||||
} from "@/renderers/types";
|
||||
|
||||
// Index — editorial/list renderer. Serif masthead + section heads, numbered
|
||||
// full-width rows, stat strip. Ported from mockups/e-editorial.html.
|
||||
|
||||
function BoardView({ children }: BoardViewProps) {
|
||||
const board = useBoard();
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const services = board.sections.flatMap((s) => s.items).filter((i) => i.kind !== "widget");
|
||||
const widgets = board.sections.flatMap((s) => s.items).filter((i) => i.kind === "widget");
|
||||
const up = services.filter((i) => board.statuses[i.id]?.status === "up").length;
|
||||
const down = services.filter((i) => board.statuses[i.id]?.status === "down").length;
|
||||
|
||||
const time = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
||||
}).format(now);
|
||||
const date = new Intl.DateTimeFormat(undefined, {
|
||||
weekday: "long", day: "numeric", month: "long", year: "numeric",
|
||||
}).format(now);
|
||||
|
||||
const cells: [string, string, string][] = [
|
||||
["Local time", time, date.split(",")[0] ?? ""],
|
||||
["Services", String(services.length), `${board.sections.length} sections`],
|
||||
["Up", String(up), "responding"],
|
||||
["Down", String(down), down === 0 ? "all green" : "offline"],
|
||||
["Widgets", String(widgets.length), "registered"],
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="mb-7 flex items-end justify-between gap-6 border-b border-border-strong pb-5">
|
||||
<h1 className="font-serif text-[52px] leading-none tracking-tight max-[620px]:text-[38px]">
|
||||
Dash.
|
||||
</h1>
|
||||
<span className="pb-1 text-right font-mono text-[11px] uppercase tracking-[0.08em] text-text-faint">
|
||||
{date}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="mb-10 grid grid-cols-2 gap-px overflow-hidden rounded-[10px] border border-border bg-border min-[760px]:grid-cols-5">
|
||||
{cells.map(([k, v, s]) => (
|
||||
<div key={k} className="bg-surface px-4 py-3.5">
|
||||
<div className="font-mono text-[10px] uppercase tracking-[0.1em] text-text-faint">{k}</div>
|
||||
<div className="mt-1 font-mono text-[24px] font-medium leading-none tracking-tight tabular-nums">{v}</div>
|
||||
<div className="mt-1.5 truncate text-[10.5px] text-text-faint">{s}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-9">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionView({ section, index, onToggle, onDelete, children }: SectionViewProps) {
|
||||
const dnd = useSectionDnd(section.id);
|
||||
return (
|
||||
<section ref={dnd.ref} style={dnd.style} className={cn(dnd.isDragging && "opacity-35")}>
|
||||
<div className="group/sec mb-0.5 flex cursor-pointer select-none items-baseline gap-3.5 border-b border-border-strong pb-2.5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Drag section"
|
||||
className="cursor-grab self-center text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
>
|
||||
<GripVertical size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="flex min-w-0 flex-1 items-baseline gap-3.5 text-left"
|
||||
>
|
||||
<span className="font-serif text-[30px] leading-none text-text-faint tabular-nums">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<h2 className="truncate font-serif text-[30px] leading-none tracking-tight">
|
||||
{section.name}
|
||||
</h2>
|
||||
<span className="font-mono text-[11px] text-text-faint">
|
||||
{section.items.length} {section.items.length === 1 ? "entry" : "entries"}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn("ml-auto self-center text-text-faint transition-transform duration-200", section.collapsed && "-rotate-90")}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete section"
|
||||
onClick={onDelete}
|
||||
className="self-center text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{!section.collapsed && children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemView({ item, index, sectionId, status, onEdit }: ItemViewProps) {
|
||||
const dnd = useItemDnd(sectionId, item.id);
|
||||
const host = item.urls[0] ? hostOf(item.urls[0].url) : "";
|
||||
const state = status?.status;
|
||||
return (
|
||||
<ItemAnchor item={item} onEdit={onEdit}>
|
||||
{(open) => (
|
||||
<div
|
||||
ref={dnd.ref}
|
||||
style={dnd.style}
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
onClick={open}
|
||||
className={cn(
|
||||
"group/row relative grid cursor-grab grid-cols-[26px_30px_minmax(0,1fr)_64px] items-center gap-3.5",
|
||||
"border-b border-border px-1.5 py-[11px] transition-colors hover:bg-surface-hover",
|
||||
"min-[760px]:grid-cols-[30px_34px_minmax(0,1.4fr)_minmax(0,1fr)_auto_70px_22px]",
|
||||
dnd.isDragging && "opacity-35",
|
||||
)}
|
||||
>
|
||||
<span className="font-mono text-[11px] text-text-faint tabular-nums">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<IconImg item={item} size={30} />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[14px] font-medium tracking-tight">{item.name}</span>
|
||||
{host ? <span className="block truncate font-mono text-[10.5px] text-text-faint min-[760px]:hidden">{host}</span> : null}
|
||||
</span>
|
||||
<span className="hidden min-w-0 truncate font-mono text-[11px] text-text-faint min-[760px]:block">{host}</span>
|
||||
<span className="hidden justify-end gap-1.5 min-[760px]:flex">
|
||||
{item.urls.slice(0, 2).map((u) => u.label ? (
|
||||
<span key={u.id} className="rounded border border-border px-[5px] py-0.5 font-mono text-[9px] uppercase tracking-[0.08em] text-text-faint">
|
||||
{u.label}
|
||||
</span>
|
||||
) : null)}
|
||||
</span>
|
||||
{item.urls.length > 0 ? (
|
||||
<span className="flex items-center gap-1.5 justify-self-end font-mono text-[10.5px] tracking-[0.05em] text-text-dim transition-opacity group-hover/row:opacity-0">
|
||||
<span className={cn("size-[5px] rounded-full", state === "up" ? "bg-up" : state === "down" ? "bg-down" : "bg-text-faint/40")} />
|
||||
{state ?? "…"}
|
||||
</span>
|
||||
) : <span />}
|
||||
<span className="hidden justify-self-end text-text-faint opacity-0 transition-all group-hover/row:opacity-100 min-[760px]:block">→</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded bg-popover p-1 text-text-faint opacity-0 transition-opacity hover:text-text group-hover/row:opacity-100"
|
||||
>
|
||||
<Pencil size={11} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</ItemAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
function WidgetView({ item, index, sectionId, onEdit }: ItemViewProps) {
|
||||
const dnd = useItemDnd(sectionId, item.id);
|
||||
return (
|
||||
<div
|
||||
ref={dnd.ref}
|
||||
style={dnd.style}
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
className={cn(
|
||||
"group/row relative grid cursor-grab grid-cols-[26px_30px_minmax(0,1fr)] items-center gap-3.5",
|
||||
"border-b border-border px-1.5 py-[11px] transition-colors hover:bg-surface-hover",
|
||||
"min-[760px]:grid-cols-[30px_34px_minmax(0,0.8fr)_minmax(0,1.2fr)]",
|
||||
dnd.isDragging && "opacity-35",
|
||||
)}
|
||||
>
|
||||
<span className="font-mono text-[11px] text-text-faint tabular-nums">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<IconImg item={item} size={30} />
|
||||
<span className="min-w-0 truncate text-[14px] font-medium tracking-tight">{item.name}</span>
|
||||
<div className="min-w-0 max-[759px]:col-start-3">
|
||||
<WidgetContent item={item} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
className="absolute right-2.5 top-2 rounded bg-popover p-1 text-text-faint opacity-0 transition-opacity hover:text-text group-hover/row:opacity-100"
|
||||
>
|
||||
<Pencil size={11} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const editorial: RendererViews = {
|
||||
BoardView,
|
||||
SectionView,
|
||||
ItemView,
|
||||
WidgetView,
|
||||
itemsGridClass: "flex flex-col",
|
||||
};
|
||||
@@ -1,13 +1,29 @@
|
||||
import { bento } from "./bento";
|
||||
import { cards } from "./cards";
|
||||
import { editorial } from "./editorial";
|
||||
import { terminal } from "./terminal";
|
||||
import type { RendererViews } from "./types";
|
||||
import type { RendererName } from "@/types";
|
||||
|
||||
// Renderer registry — settings.renderer picks the active one.
|
||||
export const rendererList = ["bento", "cards"] as const satisfies readonly RendererName[];
|
||||
export const renderers: Record<RendererName, RendererViews> = { bento, cards };
|
||||
export const rendererList = ["bento", "cards", "index", "console"] as const satisfies readonly RendererName[];
|
||||
export const renderers: Record<RendererName, RendererViews> = {
|
||||
bento,
|
||||
cards,
|
||||
index: editorial,
|
||||
console: terminal,
|
||||
};
|
||||
|
||||
export const rendererLabels: Record<RendererName, string> = {
|
||||
bento: "Bento",
|
||||
cards: "Cards",
|
||||
index: "Index",
|
||||
console: "Console",
|
||||
};
|
||||
|
||||
export const rendererDescriptions: Record<RendererName, string> = {
|
||||
bento: "mixed-size tile grid",
|
||||
cards: "uniform compact cards",
|
||||
index: "editorial list · masthead",
|
||||
console: "terminal panels · mono",
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useItemDnd, useSectionDnd } from "@/board/dnd";
|
||||
import { useBoard } from "@/board/store";
|
||||
import { IconImg, ItemAnchor, hostOf } from "@/board/primitives";
|
||||
import { WidgetContent } from "@/widgets";
|
||||
import type {
|
||||
BoardViewProps,
|
||||
ItemViewProps,
|
||||
RendererViews,
|
||||
SectionViewProps,
|
||||
} from "@/renderers/types";
|
||||
|
||||
// Console — terminal renderer. Mono everything, $ banner, bordered stat
|
||||
// panels, ## section heads, › prompt rows. Ported from mockups/f-console.html.
|
||||
|
||||
function BoardView({ children }: BoardViewProps) {
|
||||
const board = useBoard();
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const items = board.sections.flatMap((s) => s.items);
|
||||
const services = items.filter((i) => i.kind !== "widget");
|
||||
const widgets = items.filter((i) => i.kind === "widget");
|
||||
const endpoints = services.reduce((n, i) => n + i.urls.length, 0);
|
||||
const up = services.filter((i) => board.statuses[i.id]?.status === "up").length;
|
||||
const down = services.filter((i) => board.statuses[i.id]?.status === "down").length;
|
||||
const probed = up + down;
|
||||
|
||||
const time = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
||||
}).format(now);
|
||||
const date = new Intl.DateTimeFormat(undefined, {
|
||||
weekday: "short", month: "short", day: "numeric",
|
||||
}).format(now);
|
||||
|
||||
const panels: { head: string; v: string; s: string; live?: boolean; wide?: boolean }[] = [
|
||||
{ head: "clock --local", v: time, s: date, live: true, wide: true },
|
||||
{ head: "services --up", v: String(up), s: `of ${probed} probed` },
|
||||
{ head: "services --down", v: String(down), s: down === 0 ? "all green" : "needs attention" },
|
||||
{ head: "widgets", v: String(widgets.length), s: "registered" },
|
||||
{ head: "endpoints", v: String(endpoints), s: "urls indexed" },
|
||||
{ head: "uptime --probes", v: probed > 0 ? `${Math.round((up / probed) * 100)}%` : "—", s: `${up}/${probed} responding` },
|
||||
{ head: "sections", v: String(board.sections.length), s: "groups" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="font-terminal">
|
||||
<div className="mb-6 whitespace-pre-wrap rounded-md border border-border px-4 py-3.5 text-[11px] leading-[1.7] text-text-faint">
|
||||
{`$ dash status --all\n`}
|
||||
<span className="font-medium text-text-dim">{services.length} services</span>
|
||||
{` indexed · `}
|
||||
<span className="text-up">{up} up</span>
|
||||
{down > 0 ? <span>{` · `}<span className="text-down">{down} down</span></span> : ` · all green`}
|
||||
{` · `}
|
||||
<span className="font-medium text-text-dim">{widgets.length} widgets</span>
|
||||
{`\ndrag rows to reorder · click rows with multiple endpoints to choose`}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-2 gap-2.5 min-[760px]:grid-cols-4">
|
||||
{panels.map((p) => (
|
||||
<div
|
||||
key={p.head}
|
||||
className={cn("overflow-hidden rounded-md border border-border bg-surface", p.wide && "col-span-2")}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 border-b border-border px-3 py-2 text-[10px] uppercase tracking-[0.1em] text-text-faint">
|
||||
{p.head}
|
||||
{p.live ? <span className="ml-auto text-[9px] text-up">● live</span> : null}
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className={cn("font-medium leading-none tracking-tight tabular-nums", p.wide ? "text-[26px]" : "text-[22px]")}>{p.v}</div>
|
||||
<div className="mt-1 truncate text-[10.5px] text-text-faint">{p.s}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionView({ section, onToggle, onDelete, children }: SectionViewProps) {
|
||||
const dnd = useSectionDnd(section.id);
|
||||
return (
|
||||
<section ref={dnd.ref} style={dnd.style} className={cn(dnd.isDragging && "opacity-35")}>
|
||||
<div className="group/sec mb-2 flex cursor-pointer select-none items-center gap-2.5 px-0.5 py-1 text-[12px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 text-left"
|
||||
>
|
||||
<span className="text-text-faint">##</span>
|
||||
<h2 className="truncate font-bold lowercase tracking-[0.02em]">{section.name}</h2>
|
||||
<span className="text-[11px] text-text-faint">[{section.items.length}]</span>
|
||||
<span className="flex-1 overflow-hidden whitespace-nowrap text-text-faint/60" aria-hidden>
|
||||
{"─".repeat(120)}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className={cn("text-text-faint transition-transform duration-150", section.collapsed && "-rotate-90")}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Drag section"
|
||||
className="cursor-grab text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
>
|
||||
≡
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete section"
|
||||
onClick={onDelete}
|
||||
className="text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
{!section.collapsed && (
|
||||
<div className="overflow-hidden rounded-md border border-border bg-surface">{children}</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemView({ item, sectionId, status, onEdit }: ItemViewProps) {
|
||||
const dnd = useItemDnd(sectionId, item.id);
|
||||
const host = item.urls[0] ? hostOf(item.urls[0].url) : "";
|
||||
const state = status?.status;
|
||||
return (
|
||||
<ItemAnchor item={item} onEdit={onEdit}>
|
||||
{(open) => (
|
||||
<div
|
||||
ref={dnd.ref}
|
||||
style={dnd.style}
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
onClick={open}
|
||||
className={cn(
|
||||
"group/row relative grid cursor-grab grid-cols-[16px_24px_minmax(0,1fr)_auto] items-center gap-3",
|
||||
"border-b border-border px-3 py-2 text-[12.5px] transition-colors last:border-b-0 hover:bg-surface-hover",
|
||||
"min-[760px]:grid-cols-[20px_26px_minmax(0,1.2fr)_minmax(0,1fr)_auto]",
|
||||
dnd.isDragging && "opacity-35",
|
||||
)}
|
||||
>
|
||||
<span className="text-text-faint">›</span>
|
||||
<IconImg item={item} size={22} />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium lowercase">{item.name}</span>
|
||||
{host ? <span className="block truncate text-[10.5px] text-text-faint min-[760px]:hidden">{host}</span> : null}
|
||||
</span>
|
||||
<span className="hidden min-w-0 truncate text-[11px] text-text-faint min-[760px]:block">
|
||||
{host}{item.urls.length > 1 ? ` · ${item.urls.length} endpoints` : ""}
|
||||
</span>
|
||||
{item.urls.length > 0 ? (
|
||||
<span
|
||||
className={cn(
|
||||
"justify-self-end text-[10.5px] tracking-[0.04em] transition-opacity group-hover/row:opacity-0",
|
||||
state === "up" ? "text-up" : state === "down" ? "text-down" : "text-text-faint",
|
||||
)}
|
||||
>
|
||||
[ {state ?? "…"} ]
|
||||
</span>
|
||||
) : <span />}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded bg-popover p-1 text-text-faint opacity-0 transition-opacity hover:text-text group-hover/row:opacity-100"
|
||||
>
|
||||
<Pencil size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</ItemAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
function WidgetView({ item, sectionId, onEdit }: ItemViewProps) {
|
||||
const dnd = useItemDnd(sectionId, item.id);
|
||||
return (
|
||||
<div
|
||||
ref={dnd.ref}
|
||||
style={dnd.style}
|
||||
{...dnd.attributes}
|
||||
{...dnd.listeners}
|
||||
className={cn(
|
||||
"group/row relative grid cursor-grab grid-cols-[16px_24px_minmax(0,1fr)] items-center gap-3",
|
||||
"border-b border-border px-3 py-2 text-[12.5px] transition-colors last:border-b-0 hover:bg-surface-hover",
|
||||
"min-[760px]:grid-cols-[20px_26px_minmax(0,0.8fr)_minmax(0,1.2fr)]",
|
||||
dnd.isDragging && "opacity-35",
|
||||
)}
|
||||
>
|
||||
<span className="text-text-faint">›</span>
|
||||
<IconImg item={item} size={22} />
|
||||
<span className="min-w-0 truncate font-medium lowercase">{item.name}</span>
|
||||
<div className="min-w-0 max-[759px]:col-start-3 max-[759px]:py-1">
|
||||
<WidgetContent item={item} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
className="absolute right-2.5 top-2 rounded bg-popover p-1 text-text-faint opacity-0 transition-opacity hover:text-text group-hover/row:opacity-100"
|
||||
>
|
||||
<Pencil size={10} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const terminal: RendererViews = {
|
||||
BoardView,
|
||||
SectionView,
|
||||
ItemView,
|
||||
WidgetView,
|
||||
itemsGridClass: "flex flex-col",
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import type { Item, Section, Status } from "@/types";
|
||||
|
||||
export interface SectionViewProps {
|
||||
section: Section;
|
||||
index: number;
|
||||
onToggle(): void;
|
||||
onDelete(): void;
|
||||
children: ReactNode; // rendered items grid (ItemSortZone output)
|
||||
@@ -14,6 +15,7 @@ export interface SectionViewProps {
|
||||
|
||||
export interface ItemViewProps {
|
||||
item: Item;
|
||||
index: number;
|
||||
sectionId: string;
|
||||
status?: Status | undefined;
|
||||
onEdit(): void;
|
||||
|
||||
Reference in new issue
Block a user