{"id":51294278,"url":"https://github.com/schappim/stimulus_kanban","last_synced_at":"2026-06-30T13:01:27.913Z","repository":{"id":360603300,"uuid":"1249736304","full_name":"schappim/stimulus_kanban","owner":"schappim","description":"HTML-first Kanban board for Stimulus.js (Hotwire) — drag/drop, WIP limits, swimlanes, multi-card selection, custom card renderers, server-side card model, and a companion Rails gem for live multi-user moves over Turbo Streams.","archived":false,"fork":false,"pushed_at":"2026-05-27T04:24:24.000Z","size":2174,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T06:20:48.520Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/schappim.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-26T02:00:18.000Z","updated_at":"2026-05-27T04:24:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/schappim/stimulus_kanban","commit_stats":null,"previous_names":["schappim/stimulus_kanban"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/schappim/stimulus_kanban","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schappim%2Fstimulus_kanban","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schappim%2Fstimulus_kanban/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schappim%2Fstimulus_kanban/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schappim%2Fstimulus_kanban/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/schappim","download_url":"https://codeload.github.com/schappim/stimulus_kanban/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schappim%2Fstimulus_kanban/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34967627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-30T13:01:26.880Z","updated_at":"2026-06-30T13:01:27.906Z","avatar_url":"https://github.com/schappim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# stimulus_kanban\n\n[![CI](https://github.com/schappim/stimulus_kanban/actions/workflows/ci.yml/badge.svg)](https://github.com/schappim/stimulus_kanban/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/@ninjaai/stimulus_kanban?label=npm)](https://www.npmjs.com/package/@ninjaai/stimulus_kanban)\n[![stimulus_kanban_rails gem](https://img.shields.io/gem/v/stimulus_kanban_rails?label=stimulus_kanban_rails)](https://rubygems.org/gems/stimulus_kanban_rails)\n\nAn **HTML-first kanban board for [Stimulus.js](https://stimulus.hotwired.dev/) (Hotwire)**.\n\nYour board is just a semantic `\u003col\u003e` / `\u003cli\u003e` list of columns and cards. It renders without JavaScript, then progressively enhances into a full drag-and-drop board. No React, no build-time config object, no third-party kanban framework — just `data-controller=\"board\"` on a `\u003cdiv\u003e` and `data-*` attributes on the columns and cards.\n\n![Basic three-column board](docs/screenshots/basic.png)\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/wip-limits.png\"\u003e\u003cimg src=\"docs/screenshots/wip-limits.png\" alt=\"WIP limits — Doing column shows 2 / 2\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eWIP limits\u003c/b\u003e — per-column caps with \u003ccode\u003eboard:wipExceeded\u003c/code\u003e\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/swimlanes.png\"\u003e\u003cimg src=\"docs/screenshots/swimlanes.png\" alt=\"Swimlanes grouped by assignee\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eSwimlanes\u003c/b\u003e — bucket every column by any card field\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n      \u003ca href=\"docs/screenshots/incident.png\"\u003e\u003cimg src=\"docs/screenshots/incident.png\" alt=\"Incident board with P1 / P3 priority pills\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003e\u003ccode\u003eincident\u003c/code\u003e renderer\u003c/b\u003e — priority pills, affected services, age\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      \u003ca href=\"docs/screenshots/pr-review.png\"\u003e\u003cimg src=\"docs/screenshots/pr-review.png\" alt=\"PR review board with CI status, diffstat and reviewer avatars\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003e\u003ccode\u003epr\u003c/code\u003e renderer\u003c/b\u003e — CI status, +/− diffstat, reviewers\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n      \u003ca href=\"docs/screenshots/support-inbox.png\"\u003e\u003cimg src=\"docs/screenshots/support-inbox.png\" alt=\"Support inbox with channel tags and due dates\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003e\u003ccode\u003esupport-ticket\u003c/code\u003e renderer\u003c/b\u003e — channel, requester, due date\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      \u003ca href=\"docs/screenshots/leads-pipeline.png\"\u003e\u003cimg src=\"docs/screenshots/leads-pipeline.png\" alt=\"Leads pipeline with currency-formatted deal values\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003e\u003ccode\u003elead\u003c/code\u003e renderer\u003c/b\u003e — company, deal value, stage pill\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd colspan=\"2\"\u003e\n      \u003ca href=\"docs/screenshots/image-cards.png\"\u003e\u003cimg src=\"docs/screenshots/image-cards.png\" alt=\"Image cards with cover thumbnails\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003e\u003ccode\u003eimage-card\u003c/code\u003e renderer\u003c/b\u003e — cover thumbnail with click-to-zoom\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nEvery screenshot above is a [live demo page](demo/) — `npm run dev` and open `/demo/`.\n\n## What you get\n\n- **Drag-and-drop** — between and within columns, multi-card move that preserves order, programmatic drag for tests, and a cancellable `board:beforeMove` event so you can veto invalid transitions.\n\n- **Workflow controls** — WIP limits with a `board:wipExceeded` signal (fired once per crossing), per-column `accept-cards-from` allow-lists, sort modes (manual / asc / desc), and a `read-only` toggle.\n\n- **Board structure** — swimlanes (bucket every column by any card field), column collapse / hide / reorder / resize, inline \"+ Add card\" / \"+ Add column\" affordances, and a card detail panel (popover or side rail).\n\n- **Scale \u0026 search** — virtual scrolling per column (auto-on past a threshold), a global quick filter with hide-or-dim modes, a custom `setCardFilter(predicate)` hook, and a `server-side` mode for paged columns.\n\n- **Editing** — inline card editing, custom card renderers **and editors** via `\u003ctemplate\u003e` elements, and drag-to-attach files (cancellable `board:fileAttached`).\n\n- **Persistence** — set `persist-key` and column order / widths / collapse state / hidden columns / swimlane / quick filter / sort round-trip through `localStorage`.\n\n- **13 built-in card renderers** — `story`, `task`, `bug`, `incident`, `note`, `pr`, `support-ticket`, `lead`, `order`, `email-thread`, `image-card`, `cover-progress`, `gantt-stub` — plus composable sub-renderers (status pill, avatar, tags, currency, progress bar, due date…). See [Built-in card renderers](#built-in-card-renderers).\n\n- **A public `boardApi`** with movement, selection, filter, sort, swimlane, persistence, and CSV/JSON export — plus a full keyboard map for non-mouse use.\n\n## With Rails\n\nPrefer a server-driven version with live multi-user moves? The optional [`stimulus_kanban_rails`](gem/stimulus_kanban_rails) companion streams every move/edit to every connected client over Turbo Streams (Action Cable), with optimistic updates, server-side workflow guards, and an audit log ready for undo/redo.\n\nSee the **Rails \u0026 Hotwire** section below, or jump straight to [`gem/stimulus_kanban_rails/README.md`](gem/stimulus_kanban_rails/README.md). LLM-friendly usage docs live in [`skills/`](skills).\n\n---\n\n## Install\n\n**Option A — plain `\u003cscript\u003e` (no bundler).** Self-contained IIFE bundle\nwith Stimulus included; works over `file://`, a static server, anything.\n\n```html\n\u003clink rel=\"stylesheet\" href=\"https://unpkg.com/@ninjaai/stimulus_kanban/dist/stimulus_kanban.css\" /\u003e\n\u003cscript src=\"https://unpkg.com/@ninjaai/stimulus_kanban/dist/stimulus_kanban.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e StimulusKanban.start() \u003c/script\u003e\n```\n\n**Option B — npm + a bundler (Vite, esbuild, webpack…).** Stimulus is a\npeer dependency, so install it alongside:\n\n```bash\nnpm install @ninjaai/stimulus_kanban @hotwired/stimulus\n```\n\n```js\nimport { Application } from \"@hotwired/stimulus\"\nimport StimulusKanban from \"@ninjaai/stimulus_kanban\"   // resolves to dist/stimulus_kanban.esm.js\nimport \"@ninjaai/stimulus_kanban/style.css\"\n\nconst app = Application.start()\nStimulusKanban.start(app)\n```\n\n`StimulusKanban.start(app?)` registers all controllers (`board`,\n`board-column`, `card`, `card-editor`, `swimlane-header`, `column-menu`)\non the given Stimulus `Application` (or starts a new one) and returns\nit.\n\n**Option C — Rails / Hotwire (gem from RubyGems).** The\n[`stimulus_kanban_rails`](https://rubygems.org/gems/stimulus_kanban_rails)\ngem bundles this board *and* the live-sync layer, importmap-pinned — no\nJS build, no `dist/` to vendor:\n\n```bash\nbundle add stimulus_kanban_rails\n```\n\nFull setup (importmap, stylesheet, routes, optional migration) is in\nthe **Rails \u0026 Hotwire** section below.\n\n## Quick start\n\n```html\n\u003clink rel=\"stylesheet\" href=\"dist/stimulus_kanban.css\" /\u003e\n\n\u003cdiv data-controller=\"board\" style=\"height: 540px\"\u003e\n  \u003col class=\"sk-columns\"\u003e\n    \u003cli data-controller=\"board-column\"\n        data-board-column-id-value=\"todo\"\n        data-board-column-title-value=\"To do\"\u003e\n      \u003col class=\"sk-cards\"\u003e\n        \u003cli data-card-id=\"1\"\u003eBuy milk\u003c/li\u003e\n        \u003cli data-card-id=\"2\"\u003eWalk dog\u003c/li\u003e\n      \u003c/ol\u003e\n    \u003c/li\u003e\n    \u003cli data-controller=\"board-column\"\n        data-board-column-id-value=\"doing\"\n        data-board-column-title-value=\"Doing\"\u003e\n      \u003col class=\"sk-cards\"\u003e\n        \u003cli data-card-id=\"3\"\u003eWrite spec\u003c/li\u003e\n      \u003c/ol\u003e\n    \u003c/li\u003e\n    \u003cli data-controller=\"board-column\"\n        data-board-column-id-value=\"done\"\n        data-board-column-title-value=\"Done\"\u003e\n      \u003col class=\"sk-cards\"\u003e\u003c/ol\u003e\n    \u003c/li\u003e\n  \u003c/ol\u003e\n\u003c/div\u003e\n\n\u003cscript src=\"dist/stimulus_kanban.js\"\u003e\u003c/script\u003e\n\u003cscript\u003eStimulusKanban.start()\u003c/script\u003e\n```\n\nCards can be **server-rendered** (as above — parsed into the dataset on\nconnect), **loaded from a URL**\n(`data-board-card-data-url-value=\"/cards.json\"`), or set in JS via\n`element.boardApi.setCardData([...])`.\n\n## Board attributes (`data-board-*-value`)\n\n| Attribute | Meaning |\n|---|---|\n| `card-data-url` | URL returning `{ columns: [...], cards: [...] }` |\n| `card-selection` | `\"\"` \\| `\"single\"` \\| `\"multiple\"` |\n| `card-multi-select-with-click` | multi-select on plain click (no modifier) |\n| `suppress-card-click-selection` | don't select on card click |\n| `card-height` | `\"auto\"` \\| pixel number (uniform rows enable virtualisation) |\n| `column-width` | default pixel width per column (default `280`) |\n| `gap` | pixel gap between columns and between cards (default `8`) |\n| `virtual` / `virtual-threshold` | force virtual scrolling inside a column / auto-on cards-per-column threshold |\n| `height` | CSS height of the board viewport (e.g. `\"640px\"`) |\n| `get-card-id` | card-object field used as identity (default `id`) |\n| `get-column-id` | card-object field that holds the *current* column id (default `column_id`) |\n| `dom-layout` | `\"\"` \\| `\"autoHeight\"` |\n| `server-side` | server-side card model: `setCardData` represents the currently-loaded window; column totals come from `setColumnCounts` |\n| `swimlane-field` | card field used to bucket every column horizontally into swimlanes |\n| `wip-limits` | JSON map `{ columnId: number }` overlaid on `data-board-column-wip-value` |\n| `quick-filter` | initial value of the global card search |\n| `filter-mode` | `\"hide\"` (default) \\| `\"dim\"` |\n| `card-renderer` | board-wide default renderer name or template id |\n| `card-editor` | board-wide default editor template id |\n| `card-detail-template` | template id cloned into the card detail panel |\n| `detail-layout` | `\"popover\"` (default) \\| `\"rail\"` |\n| `detail-width` | rail-layout width in px (default `360`) |\n| `drag-handle-selector` | CSS selector for the drag handle inside a card (default: whole card draggable) |\n| `read-only` | disable drag/drop, inline edit, add-card |\n| `persist-key` | when non-empty, auto-save/restore column order, widths, collapse state, hidden columns, swimlane, quick filter, sort to `localStorage[\"skanban:\" + persistKey]` |\n| `add-card` / `add-column` | `true` to render the inline \"+ Add card\" / \"+ Add column\" affordances (default `false`) |\n| `accept-files` / `attachments-field` | drag-to-attach: dropping files on a card dispatches a cancellable `board:fileAttached` event |\n\n## Column attributes (`data-board-column-*-value`, on each column root)\n\n`id` (required) · `title` · `wip` · `min-count` (advisory) · `width` ·\n`collapsed` · `hidden` · `accept-cards-from` (JSON array of column ids) ·\n`disallow-drag` · `sort` (`manual` \\| `asc:\u003cfield\u003e` \\| `desc:\u003cfield\u003e`) ·\n`card-renderer` · `card-editor` · `add-card-label` · `color` · `icon`.\n\n## Card attributes (`data-card-*`)\n\n`data-card-id` (required) · `data-column-id` · `data-card-order` ·\n`data-card-renderer` · `data-card-editor` · `data-card-locked` ·\n`data-card-color` · `data-card-swimlane` · `data-card-json` (single JSON\npayload for renderers that prefer one object).\n\n## Public API — `element.boardApi`\n\nAvailable right after the `board:ready` event:\n\n```js\nboard.addEventListener(\"board:ready\", (e) =\u003e {\n  const api = e.detail.api\n  api.setCardData([{ id: 1, column_id: \"todo\", title: \"Add tests\" }])\n  api.moveCard(\"3\", { toColumnId: \"done\", toIndex: 0 })\n  api.setQuickFilter(\"bug\")\n  api.openCardDetail(\"3\")\n})\n```\n\nHighlights:\n\n- **Data:** `setCardData(cards)`, `getCardData()`, `setColumnData(cols)`,\n  `getColumnData()`, `applyTransaction({add, update, remove, move})`,\n  `setColumnCounts({colId: total})` / `getColumnCounts()` (server-side)\n- **Card selection:** `getSelectedCardIds`, `getSelectedCards`,\n  `selectCard`, `deselectCard`, `selectAllInColumn`, `clearSelection`\n- **Movement:** `moveCard(cardId, { toColumnId, toIndex })`,\n  `moveCards([cardId], target)` (multi-move preserves order),\n  `reorderCardWithinColumn`, `bulkMove`\n- **Columns:** `setColumnVisible/Collapsed/Width/Wip/AcceptFrom`,\n  `moveColumn`, `sizeColumnsToFit`\n- **Swimlanes:** `setSwimlaneField`, `getSwimlaneField`,\n  `setSwimlaneCollapsed`, `getSwimlaneCollapsedSet`\n- **Sort:** `setColumnSort`, `getColumnSort`\n- **Filter:** `setQuickFilter`, `getQuickFilter`, `setCardFilter(predicate)`\n- **Editing:** `startEditingCard(id)`, `commitEditing()`,\n  `cancelEditing()`\n- **Programmatic drag:** `beginDrag([ids], fromColumnId)` /\n  `endDrag({ toColumnId, toIndex, cancelled })` — the test-harness path\n- **WIP:** `getWipState()` → `[{colId, count, limit, over}]`\n- **Persistence:** `getBoardState`, `applyBoardState`,\n  `clearPersistedState`, `getPersistKey`\n- **Export:** `getDataAsJson()`, `getDataAsCsv({columns, swimlanes})`\n- **Detail panel:** `openCardDetail(id)`, `closeCardDetail`,\n  `isCardDetailOpen`\n\n## Events (bubble off the board element)\n\n`board:ready` · `board:cardDataChanged` · `board:columnDataChanged` ·\n`board:cardClicked` · `board:cardDblClicked` ·\n`board:cardSelectionChanged` · `board:cardEditStarted` ·\n`board:cardValueChanged` · `board:cardEditCancelled` ·\n**`board:beforeMove`** (cancellable) · `board:cardMoved` ·\n`board:cardsMoved` (multi) · `board:cardAdded` · `board:cardRemoved` ·\n`board:columnMoved` · `board:columnVisibleChanged` ·\n`board:columnCollapsedChanged` · `board:columnResized` ·\n`board:columnSortChanged` · **`board:wipExceeded`** (once per crossing) ·\n`board:filterChanged` · `board:swimlaneChanged` ·\n`board:boardStateApplied` · `board:cardDetailOpened` ·\n`board:cardDetailClosed` · **`board:fileAttached`** (cancellable) ·\n`board:columnFetchMore` (server-side).\n\n```js\nboard.addEventListener(\"board:beforeMove\", (e) =\u003e {\n  if (e.detail.toColumnId === \"done\" \u0026\u0026 !canMarkDone(e.detail.cardIds)) {\n    e.preventDefault()\n  }\n})\nboard.addEventListener(\"board:cardMoved\", (e) =\u003e save(e.detail))\n```\n\n## Custom card renderers \u0026 editors\n\nSame shape as `stimulus_grid`:\n\n```html\n\u003ctemplate id=\"story-card\"\u003e\n  \u003carticle class=\"sk-card\"\u003e\n    \u003cheader\u003e\n      \u003cspan class=\"sk-card-key\" data-bind=\"key\"\u003e\u003c/span\u003e\n      \u003cspan class=\"sk-pill\" data-bind=\"status\" data-bind-attr=\"data-status:status\"\u003e\u003c/span\u003e\n    \u003c/header\u003e\n    \u003ch4 class=\"sk-card-title\" data-bind=\"title\"\u003e\u003c/h4\u003e\n    \u003cfooter\u003e\n      \u003cspan class=\"sk-card-points\" data-bind=\"points\"\u003e\u003c/span\u003e\n      \u003cimg class=\"sk-avatar\" data-bind-attr=\"src:assignee_avatar\"\n                             data-bind-attr=\"alt:assignee_name\" /\u003e\n    \u003c/footer\u003e\n  \u003c/article\u003e\n\u003c/template\u003e\n\n\u003ctemplate id=\"story-editor\"\u003e\n  \u003cform class=\"sk-card-editor\"\u003e\n    \u003cinput data-editor-input data-editor-field=\"title\" /\u003e\n    \u003cselect data-editor-field=\"points\"\u003e\n      \u003coption\u003e1\u003c/option\u003e\u003coption\u003e2\u003c/option\u003e\u003coption\u003e3\u003c/option\u003e\u003coption\u003e5\u003c/option\u003e\n    \u003c/select\u003e\n    \u003cbutton data-editor-commit\u003eSave\u003c/button\u003e\n    \u003cbutton data-editor-cancel type=\"button\"\u003eCancel\u003c/button\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n\n\u003cdiv data-controller=\"board\"\n     data-board-card-renderer-value=\"story-card\"\n     data-board-card-editor-value=\"story-editor\"\u003e…\u003c/div\u003e\n```\n\nRenderer contract: `data-bind=\"field\"` → element text;\n`data-bind-text=\"field\"` → optionally formatted; `data-bind-attr=\"attr:field\"`\n→ set attribute (multiple pairs allowed per element).\n\nEditor contract: inputs marked `[data-editor-field=\"\u003cf\u003e\"]` are seeded\nfrom the card on open and read back into `{ field: value }` on commit.\nEnter / Tab commits; Esc cancels; the editor's\n`[data-editor-commit]` / `[data-editor-cancel]` buttons trigger the same\nhandlers.\n\n## Built-in card renderers\n\n| Renderer | Use for |\n|---|---|\n| `story` | title + key + status pill + assignee + points |\n| `task` | title + checkbox completion + due date |\n| `bug` | title + severity badge + reporter avatar |\n| `incident` | title + priority + paging count + time-since-open |\n| `note` | freeform text card with author + relative time |\n| `pr` | PR title + repo + reviewer avatars + CI dot + diff stats |\n| `support-ticket` | subject + customer + channel icon + SLA countdown |\n| `lead` | name + company + value (currency) + stage |\n| `order` | order # + status pill + total + items count |\n| `email-thread` | subject + sender + snippet + unread dot |\n| `image-card` | hero image + caption (with click-to-zoom) |\n| `cover-progress` | hero image + bottom progress bar |\n| `gantt-stub` | label + due-window bar (visual only — no real timeline) |\n\nSub-renderers (`statusPill`, `avatar`, `tags`, `currency`, `percent`,\n`progressBar`, `relativeTime`, `dueDate`, `countryFlag`, `attachments`,\n`mask`) live under `renderers.sub` so a custom card can compose them.\n\n## Keyboard\n\n`↑/↓` move active card within column · `←/→` move across columns ·\n`Cmd/Ctrl+↑/↓/←/→` actually moves the card (fires `board:beforeMove`)\n· `Enter` opens the detail / editor · `Space` toggles selection ·\n`Cmd/Ctrl+A` selects all in active column · `Cmd/Ctrl+C` copies\nselected cards as JSON · `Esc` cancels mid-drag.\n\n## Rails \u0026 Hotwire\n\nThe companion gem [`stimulus_kanban_rails`](gem/stimulus_kanban_rails)\nships a Ruby DSL for declaring boards, a Broadcastable mixin for AR\nmodels, and a Turbo Stream sync layer that wires `board:cardMoved` /\n`board:cardValueChanged` to a JSON cards endpoint:\n\n```ruby\nclass SprintBoard \u003c StimulusKanbanRails::Board\n  resource :stories\n  model    Story\n  stream_name { |_user| \"sprint:current\" }\n\n  column :todo,  title: \"To do\",  wip: 5\n  column :doing, title: \"Doing\",  wip: 3, accept_from: %i[todo]\n  column :done,  title: \"Done\",   accept_from: %i[doing]\n\n  card_field :title,  type: :string,  editable: true\n  card_field :points, type: :integer, editable: true,\n                      validate: -\u003e(v, _) { \"1–13 only\" unless (1..13).cover?(v.to_i) }\n\n  before_move -\u003e(card, from:, to:, user:) {\n    raise StimulusKanbanRails::Veto, \"needs points\" if to == :doing \u0026\u0026 card.points.blank?\n  }\nend\n```\n\n```erb\n\u003c%= stimulus_kanban_board(\n      board: SprintBoard.new(user: current_user),\n      cards: Story.current_sprint,\n      card_selection: \"multiple\") %\u003e\n```\n\nSee [`gem/stimulus_kanban_rails/README.md`](gem/stimulus_kanban_rails/README.md)\nfor install, broadcasting, audit log, and the full endpoint surface.\n\n## Project layout\n\n```\nstimulus_kanban/\n├── README.md\n├── DESIGN.md                      # architecture + DnD state machine\n├── REQUIREMENTS.md                # the original spec\n├── package.json\n├── vite.config.js                 # demo dev server\n├── vite.lib.config.js             # IIFE + ESM bundle\n├── vitest.config.js\n├── src/\n│   ├── index.js\n│   ├── controllers/\n│   │   ├── board_controller.js\n│   │   ├── board_column_controller.js\n│   │   ├── card_controller.js\n│   │   ├── card_editor_controller.js\n│   │   ├── swimlane_header_controller.js\n│   │   └── column_menu_controller.js\n│   ├── lib/\n│   │   ├── api.js\n│   │   ├── dom.js\n│   │   ├── model.js\n│   │   ├── dnd.js\n│   │   ├── virtual.js\n│   │   └── renderers.js\n│   └── styles/\n│       └── stimulus_kanban.css\n├── dist/\n├── demo/                          # 27+ HTML pages, vite-served\n├── test/                          # Vitest specs\n├── docs/images/\n├── gem/stimulus_kanban_rails/     # Rails engine\n└── skills/                        # LLM usage guides\n```\n\n## Development\n\n```bash\nnpm install\nnpm run dev          # vite-served demos at http://localhost:5173/demo/\nnpm run build:lib    # IIFE + ESM library bundles into dist/\nnpm test             # vitest\n```\n\n## License\n\nMIT — Copyright Marcus Schappi.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschappim%2Fstimulus_kanban","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fschappim%2Fstimulus_kanban","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschappim%2Fstimulus_kanban/lists"}