Kanban Column
Sunken stage column with a mono header and Card items.
import { KanbanColumn, KanbanItem,} from "@/components/kanban-column/kanban-column";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const icon = ( <IconPlaceholder lucide="SparklesIcon" tabler="IconSparkles" hugeicons="SparklesIcon" phosphor="SparkleIcon" remixicon="RiSparklingLine" />);
export default function Demo() { return ( <div className="flex flex-wrap items-start gap-3"> <KanbanColumn title="Thesis stage" meta="1/1"> <KanbanItem icon={icon} title="Initial thesis" description="User task" /> </KanbanColumn> <KanbanColumn title="Mandate stage" meta="0/3"> <KanbanItem disabled icon={icon} title="Pick tokens + weights" description="Agent task" /> <KanbanItem disabled icon={icon} title="Review mandate" description="Agent requests approval" /> </KanbanColumn> </div> );}<script setup lang="ts">import { SparklesIcon } from "@lucide/vue";import { KanbanColumn, KanbanItem } from "@/components/ui/kanban-column";</script>
<template> <div class="flex flex-wrap items-start gap-3"> <KanbanColumn title="Thesis stage" meta="1/1"> <KanbanItem title="Initial thesis" description="User task"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> <KanbanColumn title="Mandate stage" meta="0/3"> <KanbanItem disabled title="Pick tokens + weights" description="Agent task"> <template #icon><SparklesIcon /></template> </KanbanItem> <KanbanItem disabled title="Review mandate" description="Agent requests approval"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> </div></template><script lang="ts"> import * as Kanban from "$lib/components/ui/kanban-column"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";</script>
{#snippet icon()} <IconPlaceholder lucide="SparklesIcon" tabler="IconSparkles" hugeicons="SparklesIcon" phosphor="SparkleIcon" remixicon="RiSparklingLine" />{/snippet}
<div class="flex flex-wrap items-start gap-3"> <Kanban.Root title="Thesis stage" meta="1/1"> <Kanban.Item {icon} title="Initial thesis" description="User task" /> </Kanban.Root> <Kanban.Root title="Mandate stage" meta="0/3"> <Kanban.Item disabled {icon} title="Pick tokens + weights" description="Agent task" /> <Kanban.Item disabled {icon} title="Review mandate" description="Agent requests approval" /> </Kanban.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/kanban-columnpnpm dlx shadcn@latest add @edmi-ui/kanban-columnyarn dlx shadcn@latest add @edmi-ui/kanban-columnbunx shadcn@latest add @edmi-ui/kanban-columnPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/kanban-columnpnpm dlx shadcn-vue@latest add @edmi-ui/kanban-columnyarn dlx shadcn-vue@latest add @edmi-ui/kanban-columnbunx shadcn-vue@latest add @edmi-ui/kanban-columnPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vueTerminal window pnpm add @lucide/vueTerminal window yarn add @lucide/vueTerminal window bun add @lucide/vueAdd the Edmi components this one builds on:
card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kanban-column.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kanban-column.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kanban-column.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/kanban-column.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { KanbanColumn, KanbanItem,} from "@/components/kanban-column/kanban-column";
<div className="flex flex-wrap items-start gap-3"> <KanbanColumn title="Thesis stage" meta="1/1"> <KanbanItem title="Initial thesis" description="User task" /> </KanbanColumn> <KanbanColumn title="Mandate stage" meta="0/3"> <KanbanItem disabled title="Pick tokens + weights" description="Agent task" /> <KanbanItem disabled title="Review mandate" description="Agent requests approval" /> </KanbanColumn></div><script setup lang="ts">import { KanbanColumn, KanbanItem } from "@/components/ui/kanban-column";</script>
<template> <div class="flex flex-wrap items-start gap-3"> <KanbanColumn title="Thesis stage" meta="1/1"> <KanbanItem title="Initial thesis" description="User task"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> <KanbanColumn title="Mandate stage" meta="0/3"> <KanbanItem disabled title="Pick tokens + weights" description="Agent task"> <template #icon><SparklesIcon /></template> </KanbanItem> <KanbanItem disabled title="Review mandate" description="Agent requests approval"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> </div></template><script lang="ts"> import * as Kanban from "$lib/components/ui/kanban-column";</script>
<div class="flex flex-wrap items-start gap-3"> <Kanban.Root title="Thesis stage" meta="1/1"> <Kanban.Item title="Initial thesis" description="User task" /> </Kanban.Root> <Kanban.Root title="Mandate stage" meta="0/3"> <Kanban.Item disabled title="Pick tokens + weights" description="Agent task" /> <Kanban.Item disabled title="Review mandate" description="Agent requests approval" /> </Kanban.Root></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import * as React from "react";import { ArrowUpRightIcon } from "@phosphor-icons/react";import { Card } from "@/registry/edmi/ui/card";import type { Elevation } from "@/registry/edmi/ui/elevation";
type KanbanColumnContextValue = { elevation?: Elevation };
/** Depth handed from a column to its items (an item's own `elevation` wins). */const KanbanColumnContext = React.createContext<KanbanColumnContextValue>({});
type KanbanColumnProps = Omit<React.ComponentProps<"div">, "title"> & { title: React.ReactNode; /** Right header meta, e.g. "1/3". */ meta?: React.ReactNode; /** ✦ depth of every `KanbanItem` (items may override). */ elevation?: Elevation;};
/** Sunken stage column holding `KanbanItem` cards. */function KanbanColumn({ className, title, meta, elevation, children, ...props}: KanbanColumnProps) { const ctx = React.useMemo(() => ({ elevation }), [elevation]); return ( <div data-slot="kanban-column" className={cn( "flex w-[282px] flex-col gap-2 rounded-lg border border-sk-bd bg-sk-bg p-2.5 shadow-sunken", className, )} {...props} > <div className="flex items-center justify-between px-1 pt-0.5 pb-1 font-mono text-[11px] text-muted-foreground"> <span>{title}</span> {meta ? <span>{meta}</span> : null} </div> <KanbanColumnContext.Provider value={ctx}> {children} </KanbanColumnContext.Provider> </div> );}
type KanbanItemProps = Omit<React.ComponentProps<typeof Card>, "title"> & { title: React.ReactNode; description?: React.ReactNode; /** Leading icon element (an `IconPlaceholder`). */ icon?: React.ReactNode; /** Trailing element; defaults to an arrow up-right. */ action?: React.ReactNode; /** Dims the card (not yet reachable). */ disabled?: boolean;};
function KanbanItem({ className, title, description, icon, action, disabled, elevation, ...props}: KanbanItemProps) { const column = React.useContext(KanbanColumnContext); return ( <Card data-slot="kanban-item" elevation={elevation ?? column.elevation} data-disabled={disabled ? "" : undefined} className={cn( "flex-row items-center gap-2.5 px-3 py-2.5 data-[disabled]:opacity-60", className, )} {...props} > {icon ? ( <span className="grid size-7 shrink-0 place-items-center rounded-lg border border-border bg-muted [&_svg]:size-3.5"> {icon} </span> ) : null} <div className="min-w-0 flex-1"> <div className="text-[13px] font-medium">{title}</div> {description ? ( <div className="text-[11px] text-muted-foreground">{description}</div> ) : null} </div> <span className="text-muted-foreground"> {action ?? ( <ArrowUpRightIcon className="size-[13px]" /> )} </span> </Card> );}
export { KanbanColumn, KanbanItem };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { provide } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import type { Elevation } from '@/registry/edmi/ui/elevation'import { KANBAN_COLUMN_KEY } from './context'
const props = defineProps<{ /** ✦ depth of every `KanbanItem` (items may override) */ elevation?: Elevation class?: HTMLAttributes['class'] title: string /** Right header meta, e.g. "1/3". */ meta?: string}>()
provide(KANBAN_COLUMN_KEY, { get elevation() { return props.elevation },})</script>
<template> <!-- Sunken stage column holding `KanbanItem` cards. --> <div data-slot="kanban-column" :class="cn('flex w-[282px] flex-col gap-2 rounded-lg border border-sk-bd bg-sk-bg p-2.5 shadow-sunken', props.class)" > <div class="flex items-center justify-between px-1 pt-0.5 pb-1 font-mono text-[11px] text-muted-foreground"> <span>{{ title }}</span> <span v-if="meta">{{ meta }}</span> </div> <slot /> </div></template>import type { InjectionKey } from "vue";import type { Elevation } from "@/registry/edmi/ui/elevation";
export type KanbanColumnContext = { readonly elevation?: Elevation };
/** Depth handed from a `KanbanColumn` to its `KanbanItem`s (an item's own `elevation` wins). */export const KANBAN_COLUMN_KEY: InjectionKey<KanbanColumnContext> = Symbol("EDMI_KANBAN_COLUMN");<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { ArrowUpRightIcon } from '@lucide/vue'import { inject } from 'vue'import { Card } from '@/registry/edmi/ui/card'import { cn } from '@/registry/edmi/lib/utils'import type { Elevation } from '@/registry/edmi/ui/elevation'import { KANBAN_COLUMN_KEY } from './context'
const props = withDefaults(defineProps<{ /** ✦ depth of this card; defaults to the column's `elevation` */ elevation?: Elevation class?: HTMLAttributes['class'] title: string description?: string /** Dims the card (not yet reachable). */ disabled?: boolean}>(), { elevation: undefined })
const column = inject(KANBAN_COLUMN_KEY, null)// Slots: `icon` (leading), `action` (trailing; defaults to an arrow up-right).</script>
<template> <Card :elevation="props.elevation ?? column?.elevation" data-slot="kanban-item" :data-disabled="disabled ? '' : undefined" :class="cn('flex-row items-center gap-2.5 px-3 py-2.5 data-[disabled]:opacity-60', props.class)" > <span v-if="$slots.icon" class="grid size-7 shrink-0 place-items-center rounded-lg border border-border bg-muted [&_svg]:size-3.5" > <slot name="icon" /> </span> <div class="min-w-0 flex-1"> <div class="text-[13px] font-medium">{{ title }}</div> <div v-if="description" class="text-[11px] text-muted-foreground">{{ description }}</div> </div> <span class="text-muted-foreground"> <slot name="action"><ArrowUpRightIcon class="size-[13px]" /></slot> </span> </Card></template>export { KANBAN_COLUMN_KEY, type KanbanColumnContext } from "./context";export { default as KanbanColumn } from "./KanbanColumn.vue";export { default as KanbanItem } from "./KanbanItem.vue";import Root from "./kanban-column.svelte";import Item from "./kanban-item.svelte";
export { Item, Item as KanbanItem, Root, // Root as KanbanColumn,};<script lang="ts" module> import { getContext, setContext } from "svelte";
/** Depth handed from a `KanbanColumn` to its `KanbanItem`s (an item's own `elevation` wins). */ export const KANBAN_COLUMN_CONTEXT = Symbol("EDMI_KANBAN_COLUMN"); import type { Elevation } from "$lib/registry/ui/elevation/index.js";
export function setKanbanColumnCtx(ctx: { elevation: Elevation | undefined }) { setContext(KANBAN_COLUMN_CONTEXT, ctx); }
export function getKanbanColumnCtx() { return getContext<{ elevation: Elevation | undefined } | undefined>(KANBAN_COLUMN_CONTEXT); }</script>
<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, title, meta, elevation = "auto", children, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "title" | "children">> & { title: string; /** Right header meta, e.g. "1/3". */ meta?: string; /** ✦ depth of every `KanbanItem` (items may override). */ elevation?: Elevation; children?: Snippet; } = $props();
setKanbanColumnCtx({ get elevation() { return elevation === "auto" ? undefined : elevation; }, });</script>
<!-- Sunken stage column holding `KanbanItem` cards. --><div bind:this={ref} data-slot="kanban-column" class={cn( "flex w-[282px] flex-col gap-2 rounded-lg border border-sk-bd bg-sk-bg p-2.5 shadow-sunken", className )} {...restProps}> <div class="flex items-center justify-between px-1 pt-0.5 pb-1 font-mono text-[11px] text-muted-foreground"> <span>{title}</span> {#if meta}<span>{meta}</span>{/if} </div> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import { Card } from "$lib/registry/ui/card/index.js"; import ArrowUpRightIcon from "phosphor-svelte/lib/ArrowUpRight"; import type { ComponentProps, Snippet } from "svelte"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { getKanbanColumnCtx } from "./kanban-column.svelte";
let { ref = $bindable(null), class: className, title, description, icon, action, disabled, elevation = "auto", ...restProps }: Omit<ComponentProps<typeof Card>, "title" | "children"> & { title: string; description?: string; /** Leading icon (an `IconPlaceholder`). */ icon?: Snippet; /** Trailing element; defaults to an arrow up-right. */ action?: Snippet; /** Dims the card (not yet reachable). */ disabled?: boolean; /** ✦ depth of this card; defaults to the column's `elevation`. */ elevation?: Elevation; } = $props();
const ctx = getKanbanColumnCtx();</script>
<Card bind:ref data-slot="kanban-item" elevation={elevation !== "auto" ? elevation : ctx?.elevation} data-disabled={disabled ? "" : undefined} class={cn("flex-row items-center gap-2.5 px-3 py-2.5 data-[disabled]:opacity-60", className)} {...restProps}> {#if icon} <span class="grid size-7 shrink-0 place-items-center rounded-lg border border-border bg-muted [&_svg]:size-3.5"> {@render icon()} </span> {/if} <div class="min-w-0 flex-1"> <div class="text-[13px] font-medium">{title}</div> {#if description} <div class="text-[11px] text-muted-foreground">{description}</div> {/if} </div> <span class="text-muted-foreground"> {#if action} {@render action()} {:else} <ArrowUpRightIcon class="size-[13px]" /> {/if} </span></Card>Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. The column is a sunken well. elevation on the column flows to every KanbanItem card (items may override): raised (+1) bevel, floating (+2) bevel plus drop.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { KanbanColumn, KanbanItem,} from "@/components/kanban-column/kanban-column";import type { Elevation } from "@/components/ui/elevation";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const icon = ( <IconPlaceholder lucide="SparklesIcon" tabler="IconSparkles" hugeicons="SparklesIcon" phosphor="SparkleIcon" remixicon="RiSparklingLine" />);
function Sample({ elevation }: { elevation: Elevation }) { return ( <div className="flex flex-wrap items-start gap-3"> <KanbanColumn title="Thesis stage" meta="1/1"> <KanbanItem elevation={elevation} icon={icon} title="Initial thesis" description="User task" /> </KanbanColumn> <KanbanColumn title="Mandate stage" meta="0/3"> <KanbanItem elevation={elevation} disabled icon={icon} title="Pick tokens + weights" description="Agent task" /> <KanbanItem elevation={elevation} disabled icon={icon} title="Review mandate" description="Agent requests approval" /> </KanbanColumn> </div> );}
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Sample elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { SparklesIcon } from "@lucide/vue";import { KanbanColumn, KanbanItem } from "@/components/ui/kanban-column";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const,];</script>
<template> <div class="flex flex-col gap-6"> <div v-for="l in levels" :key="l.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ l.label }}</p> <div class="flex flex-wrap items-start gap-3"> <KanbanColumn :elevation="l.value" title="Thesis stage" meta="1/1"> <KanbanItem title="Initial thesis" description="User task"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> <KanbanColumn :elevation="l.value" title="Mandate stage" meta="0/3"> <KanbanItem disabled title="Pick tokens + weights" description="Agent task"> <template #icon><SparklesIcon /></template> </KanbanItem> <KanbanItem disabled title="Review mandate" description="Agent requests approval"> <template #icon><SparklesIcon /></template> </KanbanItem> </KanbanColumn> </div> </div> </div></template><script lang="ts"> import * as Kanban from "$lib/components/ui/kanban-column"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const, ];</script>
<div class="flex flex-col gap-6"> {#each levels as l (l.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{l.label}</p>{#snippet icon()} <IconPlaceholder lucide="SparklesIcon" tabler="IconSparkles" hugeicons="SparklesIcon" phosphor="SparkleIcon" remixicon="RiSparklingLine" />{/snippet}
<div class="flex flex-wrap items-start gap-3"> <Kanban.Root elevation={l.value} title="Thesis stage" meta="1/1"> <Kanban.Item {icon} title="Initial thesis" description="User task" /> </Kanban.Root> <Kanban.Root elevation={l.value} title="Mandate stage" meta="0/3"> <Kanban.Item disabled {icon} title="Pick tokens + weights" description="Agent task" /> <Kanban.Item disabled {icon} title="Review mandate" description="Agent requests approval" /> </Kanban.Root></div> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
KanbanColumn |
title |
ReactNode (Vue/Svelte: string) |
required |
meta |
same as title; right header meta, e.g. 1/3 |
none | |
elevation ✦ |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
|
…props |
div props, sunken container 282px wide |
||
KanbanItem |
title |
ReactNode (Vue/Svelte: string) |
required |
description |
same as title |
none | |
icon |
React ReactNode; Vue slot icon; Svelte icon snippet |
none | |
action |
React ReactNode; Vue slot action; Svelte action snippet |
arrow up-right | |
disabled |
boolean; dims to 60% |
false |
|
elevation ✦ |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card.
- Install:
shadcn add @edmi-ui/kanban-column.