- .btn-icon: transparent background, muted icon that turns accent on
hover -- matching .title-back's treatment exactly, instead of a
solid accent-filled square.
- The "all tasks" tab's "Add card" button is now the same .btn-icon
as the kanban/inbox/status ones, so every "add" action in the app
looks and behaves the same.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The kanban column, sidebar inbox, and add-status forms all had their
own "Add" submit button; with a column-per-status now, that's a lot of
identical labels on screen at once. Replaced with a small square "+"
icon button (.btn-icon), each keeping a descriptive title + aria-label
(dynamic, so it reads "Adding…" while the request is in flight) since
the visible glyph alone isn't an accessible name.
.btn-icon is excluded (":not(.btn-icon)") from the generic submit
button rule and the .field-row/.kanban__new compact-button overrides,
rather than out-specificity'd -- same reasoning as .field's low
specificity against input:focus: it shouldn't depend on source order
which one wins. .kanban__new's own override is now dead (both of its
two buttons are icons) and removed outright.
Left the "Add card" button on the "All tasks" tab as text -- it's a
lone, full-width primary action in a larger form, not one of several
compact icon-sized ones crowded together.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each column gets its own form at the bottom (styled like the sidebar
inbox's), creating the card directly in that status, appended after
its existing cards.
- API: POST /projects/{id}/cards takes an optional status_id, which
must belong to the project (422 otherwise); omitted, it still
defaults to the project's first status as before. Position is
already "end of that status" for free -- createInProject() already
ranks by (owner, project, status).
- cards store: add() takes an optional statusId, forwarded as
status_id when given.
- ProjectView: one draft string per status (keyed by status id) so
typing in one column doesn't touch another's, mirroring the
per-status independence the columns already have for reordering.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generalized .card-view__back into .title-back and applied it to:
- ProjectView: arrow back to the dashboard, inline before the title
(previously no back link at all here -- the app bar's brand already
covers it, but this is more discoverable and matches the others).
- ProjectConfigureView: arrow back to the project, replacing the
separate "Back to project" button that used to sit in the actions
corner next to Manage.
- ProfileView: arrow back to the dashboard, replacing the plain-text
link removed in an earlier commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clicking a card -- in the "all tasks" list, a kanban column, or the
inbox -- now opens /cards/:id instead of editing the text in place:
- CardRow's text is now plain (its RouterLink wraps the text + status
badge; the delete button stays a sibling so it isn't nested inside
the link). KanbanCard is now itself a RouterLink.
- New CardView.vue: header follows the project view's layout, but the
back link sits inline inside the title (before the card's text)
rather than off in the actions corner, since it isn't paired with a
manage menu here. Inbox cards have no project to link back to, so
they go to the dashboard instead. Below the header, an "Edit text"
section (styled like the project rename form) replaces the inline
editing that used to live in the list row, and a delete button in
the header actions replaces CardRow's per-row delete for cards
reached via kanban/inbox (which never had one).
- No backend changes: GET/PATCH/DELETE /cards/{id} already existed.
- Renamed .card-row__status to the more general .status-badge, now
shared by the list row and the card view.
- cards store: dropped setText, now unused now that editing goes
through a direct PATCH + store refresh in CardView instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Pull the status-management feature (drag reorder, add, delete with
reassignment) out of ProjectConfigureView into its own StatusManager
component. It fetches its own status list independently, so the view
is left with just page chrome and the rename form (316 -> 106 lines).
- Define ColumnChange once in lib/cardOrder.ts instead of duplicating
the same type in AppSidebar.vue and ProjectView.vue.
- Add composables/useDialog.ts for the Escape-to-close + focus-on-open
behaviour shared by every confirm/reassign modal (and, without a
focus target, plain dropdown menus). Used by ProjectManageMenu's
delete-confirmation modal + its own menu, and StatusManager's
reassignment modal.
The root cause of 'some inputs show a focus outline, most don't': the
global input:focus/textarea:focus/select:focus rule (outline:none,
border-color: accent) has specificity (0,0,1,1). Every per-component
rule shaped '.wrapper input { border: 1px solid var(--border) }' (.form
input, .field-row input, .kanban__new input, .sidebar__select select,
.modal__field select) ties it exactly, and -- being unconditional --
silently won that tie by simply appearing later in the file, so the
input's border stayed var(--border) forever regardless of focus. Only
two controls escaped: .project-card__name-input (a class applied
directly to the element, (0,0,1,0), too low to ever win the tie) and
.card-row__text (has its own .card-row__text:focus, (0,0,2,0),
genuinely higher). Confirmed with a live computed-style test against
the real stylesheet before and after.
Fix: one canonical .field class (plus .field--compact for inline 'add'
rows and the sidebar, .field--autosize for the dashboard's JS-grown
textarea), applied directly to the <input>/<textarea>/<select> itself
in every view -- structurally immune to the same tie, since a bare
class can never out-specificity input:focus. This also collapses five
near-duplicate rules (each with its own padding/radius/font-size) into
one definition, and fixes .kanban__new input's stray
background: var(--surface) (every other field uses var(--bg); this one
nearly matched its own var(--surface) sidebar panel).
Also: .btn-danger hardcoded #b3261e/#fff instead of the already-
existing var(--error)/var(--accent-text) tokens, so danger buttons
didn't adapt in dark mode like every other error-coloured element;
now they do. Added .field:disabled styling (opacity/cursor), matching
buttons -- latent until now since no input bound :disabled yet.
Second pass, promoting repeated-but-consistent raw values to tokens:
- border-radius: 6/7/8/10/12/999px -- the stray 7px (.sidebar__link)
folded into the 6px tier -- become --radius-sm/md/lg/xl/pill.
- disabled/ghost opacity: 0.6 was used for every disabled button
except .status-row__delete's 0.4 (now unified) and drag-ghost states'
0.5 (semantically different, kept separate) -- --opacity-disabled/
--opacity-ghost.
Font-size and spacing values also repeat but don't reduce to a clean
scale without arbitrary judgement calls either way -- left alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.project-head__actions stretches full width on mobile along with the
title (align-items: stretch), but its own content was left-aligned by
default. Add justify-content: flex-end there under the same
breakpoint, and wrap ProjectView's ProjectManageMenu in a
.project-head__actions div (it wasn't before) so both views share one
selector for this instead of ProjectView needing its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- .tabs__panel--narrow (42rem, the project view's 'All tasks' tab):
removed the class and its rule, same reasoning as the config
sections -- the page around it is already full-width.
- Profile now sets meta.wide, so it uses .app__main--wide instead of
the default .app__main's 32rem cap, matching the dashboard/project/
configure views instead of sitting narrow like the login form.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ProjectView: title is now a plain <h1>, no longer inline-editable
(dropped titleDraft/saveTitle/patchProject and the input markup --
nothing else used patchProject).
- ProjectConfigureView: new 'Project name' section above 'Statuses',
a small PATCH /api/projects/:id form. On success it also calls the
projects store's fetchProjects(), since the dashboard grid and the
sidebar's project dropdown read from that store and otherwise
wouldn't pick up the new name (or the project's new alphabetical
position) until some unrelated reload.
- style.css: dropped the now-dead .project-head__title input rules
(both views render a plain heading now); .project-head__title gains
word-break so a long static title still wraps instead of overflowing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New /projects/:id/configure view, linked from a new 'Configure' item on
the project view's Manage menu.
Backend:
- CardStatusRepository/CardStatusController gain full CRUD: create
(appended at the end), reorder (dense positions, like card
ordering), and delete.
- Deleting a status with cards attached is rejected with 409 and
error.details.card_count, rather than hitting the existing FK
RESTRICT constraint -- retrying with { reassign_to: <status id> }
moves those cards to that status first (CardRepository::
reassignStatus, appended after the destination's existing cards)
and deletes in one transaction (CardStatusRepository::transaction,
shared PDO connection across repositories).
- The last status in a project can't be deleted, since a project card
is required to have one.
- Routes: POST/DELETE .../statuses(/:id), PUT .../statuses/order.
- 14 new CardStatusTest cases covering all of the above.
Frontend:
- ProjectConfigureView.vue: header (title, back-to-project link, the
shared Manage menu) + a vuedraggable status list (reorder persists
the whole new order) with a delete button per row and an add-status
form. A row's plain delete either succeeds immediately or, on 409,
opens a modal to choose a different status before retrying the
delete with reassign_to.
- Extracted ProjectManageMenu.vue (the Manage dropdown + delete-project
modal) out of ProjectView so both views share it; it now also has a
Configure link (hidden on the configure page itself).
- ApiError gains a cardCount getter (details.card_count), mirroring
the existing retryAfter getter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove the '<- All projects' link at the top -- the sidebar's
Dashboard link already covers that.
- Remove the description textarea and all of its state/save logic
(descriptionDraft, saveDescription, patchProject's description
field); the backend field and API are untouched.
- Drop .project__chrome's 42rem max-width, which existed only to keep
the now-gone description prose readable. The header (title + Manage
menu) now spans the full wide-layout width, so Manage sits at the
true top right instead of a narrower centred column.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- DashboardView: a "New project title" form now sits directly below the
project grid (creating one stays on the dashboard -- the grid and the
sidebar dropdown pick it up via the shared projects store, no navigation).
- AppSidebar: the per-project RouterLink list is replaced by a <select>. It's
a v-model-bound writable computed (selectedProjectId): the getter tracks
route.params.id so it reflects whichever project is open, the setter
router.pushes to the chosen one -- so it doubles as a project switcher from
anywhere, not just a picker from the dashboard. Removed the now-unused
.sidebar__link--project/.sidebar__projects/.sidebar__form CSS; added
.sidebar__select, and .form--new-project back for the dashboard form.
- ProjectView: activeTab now initialises to 'kanban' instead of 'all'. The
tabs array (and so the tab bar's DOM order, All tasks first) is untouched --
only the default selection changed.
Verified via headless Chrome: sidebar has no per-project links (a <select>
listing both projects instead); the new-project form follows the grid and
creating from it stays on /dashboard while the grid and dropdown both update;
selecting a project in the dropdown navigates there and the dropdown reflects
it once there; a project opens on Kanban with the tab bar still reading "All
tasks, Kanban" in that order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A card either sits in its owner's inbox (project_id AND status_id both NULL)
or belongs to exactly one project with a status in it (both set) -- enforced
by a CHECK constraint, never one without the other. The inbox is global to a
user now, not per-project: cards can move from a project into the inbox and
back into any status column of any project.
Backend
- migrations/009: rebuilds `cards` (SQLite can't relax NOT NULL / add a CHECK
in place) with a nullable project_id, a new owner_id (cards need direct
ownership once they can have no project), and the CHECK constraint. Cards
that had no status (the old per-project inbox) move to the new global inbox.
status_id's FK is now ON DELETE RESTRICT, not SET NULL -- nulling it alone
would violate the invariant, and there's no status-delete endpoint anyway.
- CardRepository: "column" is now (owner_id, project_id, status_id); every
method that dealt with a project's columns is generalised to also cover the
inbox and cross-project moves (orderColumn, idsInColumn, repack, ...).
- CardController/routes: single-card and ordering routes move to global,
since a card may have no project to nest them under --
GET/PATCH/DELETE /api/cards/{id}, PUT /api/cards/order (body now takes
project_id + status_id, both null for the inbox). New GET/POST
/api/inbox/cards. PATCH no longer accepts status_id -- moving a card, in or
out of a project, is exclusively PUT /api/cards/order now. A card created
directly in a project (POST /api/projects/{id}/cards) lands in its first
status, since a project card can't have no status.
- Tests: ProjectTest/CardStatusTest updated for the new routes; CardOrderTest
rewritten with full inbox/cross-project coverage. 57 tests pass.
Frontend
- New stores/inbox.ts (the global inbox) and lib/cardOrder.ts (the shared
PUT /api/cards/order call, used by both the sidebar and a project's board).
- AppSidebar: an Inbox section under the project list -- a vuedraggable list
in the same "kanban" drag group as every project's kanban columns, so a
card drags straight from the sidebar into whichever project is open, or
back out. (The empty-inbox state needed a real bugfix: it wasn't rendering
a <draggable> at all, so there was nowhere to drop a card back into an
empty inbox.) A drop reloads the inbox and, if a project is open, its cards.
- ProjectView's kanban board drops its synthetic Inbox column -- just the
real statuses now.
- DashboardView simplified to a plain grid of project tiles (name + card
count); its per-project "New" section is gone, since a project card can no
longer have no status.
- stores/cards.ts: patch/remove move to the global /api/cards/{id} routes.
Verified end-to-end against the rebuilt container (existing per-project-inbox
cards correctly migrated to the global inbox, 0 invariant violations) and the
dev server via headless Chrome: sidebar inbox -> project A "To do" -> back to
inbox -> project B "Done", full journey confirmed via the API at each step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New --inbox-bg token (light and dark) applied via .kanban__col--inbox so the
inbox reads as distinct from "To do" / "Doing" / "Done".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Statuses
- Migration 006: card_statuses table (project-scoped) and cards.status_id, a
nullable FK with ON DELETE SET NULL. Every new project is seeded with
"To do" / "Doing" / "Done"; GET /api/projects/{id}/statuses lists them.
- New cards have no status -- they sit in an "inbox" until moved.
Project view
- Full-width and tabbed: "All tasks" (a flat list, sorted by name
case-insensitively) and "Kanban" (Inbox plus one column per status).
- Drag a card within or between columns to reorder / restatus; the Inbox
column has its own name + Add form.
Ordering
- Migration 007: `position` is now a dense 0..n-1 rank within a
(project_id, status_id) column, not a project-wide order. New composite
index idx_cards_project_status_position; existing rows re-ranked.
- PUT /api/projects/{id}/cards/order takes { status_id, card_ids } and sets one
column's contents and order, re-parenting moved-in cards and re-packing their
source column in a single transaction. PATCH status_id appends the card to the
end of the destination column.
58 phpunit tests pass; the frontend type-checks and builds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>