Work-surface components: Kanban, Milestone roadmap, Timeline, Dependency graph, People picker — and TUX inside apps without Nuxt #61
No reviewers
Labels
No labels
idea
points
1
points
13
points
2
points
3
points
5
points
8
priority
p0
priority
p1
priority
p2
priority
p3
state
blocked
state
done
state
in-progress
state
ready
state
review
state
triage
status
declined
status
in-progress
status
planned
status
proposed
status
shipped
status
under-review
type
bug
type
epic
type
feature
type
spike
type
story
type
task
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Blocks
You do not have permission to read 1 dependency
Reference
tti/tti-ux!61
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/work-surface-components"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Nine components for planning and doing work, built for TTI Code's per-repo Roadmap tab and usable anywhere. That tab is a TUX app mounted in a shadow root inside Forgejo pages; see nis/forgejo-stack
docs/design/work-surface-island.md. Plus the fixes TUX needed to run a panel inside an app that isn't Nuxt.New components
TuxKanbanBoard/TuxKanbanLane/TuxKanbanCard/components/kanbanmoveintent.canDroprefuses a lane with a stated reason. Promoted from AI Studio's kanban primitive, which listed keyboard drag and drop as its prerequisite for promotion.TuxMilestoneRoadmap/components/milestone-roadmapTuxTimeline/components/timelineTuxDependencyGraph/components/dependency-graphTuxPeoplePicker/components/people-pickerChanged (existing pages keep their current behaviour)
selectablesteps that emitselect;lockedreason;busyIndex;variant="path", a compact form for panels and cards;torenders a real link (a:is="'NuxtLink'"string rendered an unknown<nuxtlink>element).hrefcrumbs render as plain links, and crumbs with neithertonorhrefare buttons that emitnavigate.titlenow grows instead of overflowing Nuxt UI'sh-6row.Rules every new component follows
NuxtLinkin the new components, nouseRoute, nouseHead.document-global queries and no teleports; helpers are imported by relative path.Checks run on this branch
nuxt typecheck: clean.eslint .: clean.vitest: 254 passed, 24 files, including mounted tests for each new component and for the changed contracts.audit:tokens: OK.generate && audit:a11y: every new or changed page passes. The two remaining violations are pre-existing and not touched here:/components/code-maroon(role="error") and/install(empty headings).Left for 3.0's palette work (not changed here):
tti-dark,--text-on-brandis white on the light-teal--brand-primary(2.4:1).--wash-brand-*stays maroon, because it is computed on the theme root.The new components mix their tints from
--brand-primaryand put--surface-pagetext on brand fills, so they pass either way.Merging: this is based on
main, not on thestatus-palette/ 3.0 work. The only overlaps with 3.0 are appended rows intuxCatalog.ts,design/components.mdand the CHANGELOG. The forge's island can pin this branch's commit until it's merged.Promoted from TTI AI Studio's kanban primitive, with what it listed as the prerequisite for promotion: keyboard drag-and-drop. - The board holds no data and emits one move({cardId, fromLaneId, toLaneId, beforeCardId}) intent; nothing fires for a drop where the card already was. - Keyboard: Space lifts, arrows choose lane and position, Space/Enter drops, Escape (which never reaches a surrounding dialog) or Tab cancels; every step is spoken through a live region; the card keeps focus after the parent re-renders it in its new lane. - canDrop(cardId, toLaneId, fromLaneId) returns true or a sentence; a refusing lane shows the sentence while the drag is over it and the drop is cancelled and announced (e.g. 'Done' for work still waiting on something). - Pointer: arms after 4px, never from a control inside the card, swallows the click that ends a drag, auto-scrolls lane and board; on touch a vertical swipe still scrolls the lane. Read-only boards (disabled) open cards only. - Shadow-DOM safe: hit-testing asks the card's root node and nothing touches document.body — the forge mounts TUX inside a shadow root. The drag context lives in TuxKanbanBoard.vue's exporting script so hosts that don't auto-import TUX composables can use it. - Cards are named groups, not buttons: card bodies often hold their own buttons (nested interactive controls). Narrow containers snap one lane per screen. Showcase at /components/kanban; mounted tests for the keyboard contract, refusals, read-only, Escape containment and one pointer drag.A GitLab/GitHub-Projects-style roadmap view, built as the forge Roadmap tab's "Timeline" view: areas of work and their items as rows, bars from when work started to when its milestone is due. - Sticky label column (buttons, emit `select`) beside a horizontally scrolling track: week / month ("Oct 2026") / quarter ("2026-Q4") ticks, gridlines, dashed milestone lines flagged in the header, a today line. Scale is auto from the visible span; the track fills wide containers and scrolls in narrow ones; on mount it scrolls today into view. - start+end → bar (progress fill, done muted); end only → diamond; start only → open-ended fading bar; neither → the Unscheduled block (never dropped). Bars cut by the range get dashed edges; rows fully outside it become a focusable chevron at that edge. Markers emit `selectMarker`. - Dependencies: SVG elbow arrows from the end of `from` to the start of `to`. When `from` ends after `to` starts the arrow is red + dashed with a <title> ("… can't start before … ends"), both bar edges turn red, and both labels carry a warning icon and "schedule conflict" text. Arrows with missing rows are skipped. - Every bar / point / marker is a button whose accessible name carries its dates ("Forgejo v17 hop — Oct 1 to Oct 29, 2026, 40% done") and what it waits on. Hover/focus shows a tooltip clamped inside the component and flipped above near the bottom. Arrow keys move between rows and along a row; Escape hides the tooltip. - Container queries (labels never exceed ~42% of a narrow container; truncated labels show in full on hover/focus), reduced-motion aware, TUX tokens only. - Island-safe: no Nuxt-only APIs, no document-global queries, no teleports; the pure math (date → x, ticks, range, sections, impossible-order detection, tooltip / flag / arrow geometry) is exported from the component's plain <script> block, so a host imports it with the component and nothing relies on Nuxt auto-imports. - Optional props beyond the brief: selected, loading, ariaLabel, maxHeight. Slots: #label="{ row }", #empty. Showcase /components/timeline (roadmap with an impossible dependency, week sprint, quarter plan with an explicit range, 340px, empty / loading / all-unscheduled, and how it differs from TuxActivityTimeline). Tests: tests/components/tux-timeline.nuxt.test.ts (pure helpers + mounted emits, keyboard, conflict drawing, states).A search box over a list of people (TuxAvatar, name, a subtitle such as "Student Technician · NET", a TuxBadge status such as "Needs access"), built for the forge's Roadmap item panel, where it sits in a UPopover inside a modal <dialog> side panel. - WAI-ARIA APG combobox with an always-visible listbox: focus stays in the input, aria-activedescendant tracks the active option; options carry aria-selected / aria-disabled. - Keyboard: arrows wrap, Home/End (modifier keys stay caret keys), Enter toggles (never submits a form), Escape emits close with propagation stopped and default prevented so the enclosing dialog stays open. IME composition is left alone. - v-model (ids) + v-model:query (emitted on every keystroke). Local filtering (name/subtitle/login, token- and accent-insensitive) unless `remote`, where the picker never filters and nothing stale is made active until the new results land. - toggle(id, selected) per changed id; single select replaces (the old person reported deselected first). - selectedFirst sorts by the selection as of the last query / outside v-model change, so rows never jump under the pointer mid-pick. - Loading keeps the list (aria-busy, spinner); empty text or #empty slot only when not loading; #footer slot; polite status line. - Directory-order initials ("Rojo, Alex" -> AR) via tuxPersonInitials. - 20rem default width (--tux-people-picker-width), inline-size container: status badges drop under the text below 21rem. - Island-safe: no Nuxt-only APIs, no document queries, no Teleport; helpers in app/utils/tuxPeople.ts imported by relative path. Catalog entry, components.md row, showcase page (popover in a mock item panel with a read-only switch, remote search with a fake delay and a directory-wide footer, single select, loading/empty/failed/disabled states, phone widths, keyboard table), mounted contract tests and helper unit tests.A layered, left-to-right graph of work items where an arrow means "waits on" (edge {from, to}: `to` waits on `from`). Built for the forge's Roadmap tab, where the data are Forgejo issue dependencies, cross-repo links included. Separate from TuxDiagram (Mermaid source written by hand): this one is data-driven, lays itself out, and every card is a button. Layout (app/utils/tuxDependencyGraph.ts, pure, no DOM): - links cleaned first: self-links, links to unknown ids, malformed and repeated links are dropped and counted, never thrown on - unlinked items go to a "Not linked" strip, not the graph - loops = Tarjan SCCs of 2+ items; drawn in a framed Loop band under the graph in cycle order, red links, a plain-words banner naming each member; items downstream of a loop are placed normally, flagged - columns by longest path (Kahn over the condensation, a loop taking one column per member), placeholders for column-skipping links, barycenter sweeps keeping the order with the fewest crossings - orthogonal routing with per-gap lanes; a long link that changes rows runs off the row centre line so it can't be read as another card's link Component: cards are real buttons (select on click/Enter/Space), Tab in step order, arrow keys to the nearest card, hover/focus/`selected` traces the whole upstream + downstream chain and dims the rest, "Waiting on N" (open blockers only), "Unblocked", done/external/loop cues that don't rely on colour, a hidden per-card description of what it waits on and what needs it. Optional props beyond the brief: loading, ariaLabel; #empty slot. Island-safe: explicit relative imports, no Nuxt APIs, no document queries, no teleports; container queries; reduced motion. Registered in the catalog, components.md (both tables) and a showcase page.One section per milestone: due wording ("due Oct 29 · in 36 days", "12 days overdue"), "4 of 9 done" + progress bar, and the items as buttons that emit select(item.id). Open milestones by due date, then undated, then closed (latest first); unscheduled items form a final "No milestone" section. Closed and 100%-done milestones start collapsed; the start state then sticks across re-renders. Header is a heading + disclosure button (aria-expanded/controls, described by due + progress); collapsed bodies use hidden="until-found". Arrow/Home/End move between headers and rows. Overdue is said in words, done/area carried by visually-hidden text or the row's name. Plain-Vue safe for the forge island: no Nuxt APIs, no document queries, no teleport. Container queries from ~340px up. Pure helpers (tuxMilestoneOrder, tuxMilestoneDue, tuxMilestoneProgress, tuxMilestoneStartsCollapsed) and the TuxMilestone* types are exported from the SFC's plain <script> block. Showcase page covers flagship, 340px, read-only, #item slot, odd data, loading and empty; mounted test covers ordering, due wording, emits, keyboard, collapse, slots, states.torenders a real link be714b643c- color-mix washes on the ladder {4,6,8,12,18,22,35,50}: 5→6, 10→8, 25→22, 45/55→50 across the new components and the stepper's path variant. - No new raw colour literals: the shadow tokens need no rgba() fallback, a mask gradient uses the black keyword, and example issue refs in comments (#184 reads as a hex literal to the ratchet) became #n / #12. - kit/ports/manifest.json gains the seven new components (react: never ported); the ports-sync workflow queues them after merge. Pre-existing drift on main is left to that workflow. - TuxKanbanBoard: lane stepping without an unchecked index (nuxt typecheck).🔧 Security-gate fix map
The gate failed on these dependency findings — fastest path to green for each:
@nuxtjs/mdc@tiptap/corenpm update core --package-lock-onlyjs-yamlnpm update js-yaml --package-lock-onlysharpnpm update sharp --package-lock-onlysvgonpm update svgo --package-lock-onlyPosted once per head commit by the baseline gate (M2). A Renovate PR that only touches a manifest with no lockfile change is a broken pre-2026-08-06 artifact — check its diff before merging.
AI review · advisory
Verdict: 5 things worth fixing (1 high · 3 medium · 1 low).
Findings that didn't map to a diff line:
app/utils/tuxCatalog.ts:280· HIGH — New components missing from the component catalogThe newly added components (TuxDependencyGraph, TuxKanbanBoard, TuxKanbanCard, TuxKanbanLane, TuxMilestoneRoadmap, etc.) are not listed in
tuxCatalog, causing the catalog consistency test (tests/tux-catalog.test.ts) to fail and breaking the single source of truth for navigation, showcase, and documentation.⚑ panel: GPT-OSS 120B · Gemma 4 31B · Devstral 2 123B · Laguna S 2.1 — 16 distinct (from 18 reviewer findings), 5 confirmed, 3 below threshold, 8 refuted · web: not used · context: 8 files under review · 89 codebase · 5 standards chunks (best-grounded: Gemma 4 31B; smaller windows saw less)
Panel debate — how this review was reached
Grounding — context: 8 files under review · 89 codebase · 5 standards chunks (best-grounded: Gemma 4 31B; smaller windows saw less)
Round 1 — independent reviews
Math.random()is used for accessibiGrouping — 18 reviewer findings describe 16 distinct defects; reviewers who found the same defect independently count as support.
Round 2 — cross-examination
GPT-OSS 120B#1New components missing from the component catalog · confirmed: Gemma 4 31B, Devstral 2 123B, Laguna S 2.1 · refuted: — · support 3GPT-OSS 120B#2hrefattribute applied to<button>elements · confirmed: Gemma 4 31B, Devstral 2 123B · refuted: Laguna S 2.1 · support 2Laguna S 2.1#2Math.random() used for aria-labelledby ID — SSR hydration mismatch and inconsist · also raised by: Gemma 4 31B · confirmed: GPT-OSS 120B, Devstral 2 123B · refuted: — · support 3Devstral 2 123B#1Missing validation forcrumb.toandcrumb.href· confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Devstral 2 123B#2Missing validation fortagOffunction · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Laguna S 2.1#1Math.random() used for aria-describedby ID — SSR hydration mismatch and inconsis · also raised by: Gemma 4 31B · confirmed: GPT-OSS 120B, Devstral 2 123B · refuted: — · support 3Devstral 2 123B#3Missing validation fornodesandedgesprops · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Laguna S 2.1#3Custom focus ring conflicts with the universal --shadow-focus system · confirmed: GPT-OSS 120B, Gemma 4 31B, Devstral 2 123B · refuted: — · support 3GPT-OSS 120B#4Missingcontainer-typeon the root element · confirmed: Devstral 2 123B · refuted: Gemma 4 31B, Laguna S 2.1 · support 1GPT-OSS 120B#3Potential stale detection ofnavigatelistener · confirmed: Devstral 2 123B · refuted: Gemma 4 31B, Laguna S 2.1 · support 1Gemma 4 31B#3Use of internal Vue instance API · confirmed: Devstral 2 123B · refuted: GPT-OSS 120B, Laguna S 2.1 · support 1Devstral 2 123B#8Missing validation forunscheduledprop · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Not posted (support < 2, or contested at a severity where one refutation vetoes)
GPT-OSS 120B#4Missingcontainer-typeon the root element (support 1)GPT-OSS 120B#3Potential stale detection ofnavigatelistener (support 1)Gemma 4 31B#3Use of internal Vue instance API (support 1)Raised but refuted (left out of the review above)
Devstral 2 123B#1Missing validation forcrumb.toandcrumb.href— ThetoOffunction simply returnsc.toor defaults to"/"for the home crumb. Vue‑Router toleratesundefinedor missing strings, and the componDevstral 2 123B#2Missing validation fortagOffunction —tagOfrelies on the truthiness oftoOfand the presence ofc.hrefto decide which element to render. An empty string evaluates to false, which gDevstral 2 123B#3Missing validation fornodesandedgesprops — Propsnodesandedgesare typed as arrays and the component already handles empty collections (e.g., showing a placeholder when there are no nodesDevstral 2 123B#8Missing validation forunscheduledprop — Theunscheduledprop is optional and may be omitted; when provided it is typed as an array of items. Empty or missing values are already handled bySynthesis — Laguna S 2.1 wrote the final review from 5 confirmed findings (+3 below threshold) · promotion: support ≥ 2, and no refutation at high severity.
Transcript
rv-20260924163107-e87577— full round outputs, web results, and model reasoning are viewable by anyone with access to this repository via the AI gateway.Advisory — never a merge gate. Findings are ordered by how well the panel's own evidence checks out, strongest first. React 👍/👎 on any inline comment to tell it whether it was worth flagging — that is the only feedback this system gets, and every threshold in it is tuned from those reactions. Transcript
rv-20260924163107-e87577.@ -84,14 +95,37 @@ withDefaults(defineProps<Props>(), {/><span>{{ crumb.label }}</span></NuxtLink><componentapp/components/TuxBreadcrumbs.vue:98· MEDIUM —hrefattribute applied to<button>elementsThe home crumb template always binds
:href="crumb.href"even when the rendered tag is a<button>, producing an invalidhrefattribute on a non-anchor element that browsers ignore and that fails accessibility checks.Proposed replacement (one-click ⚡ Apply on the findings board at the top of this PR):
Evidence: partly verified — two or more reviewers, quote or anchor verified (panel 3/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
@ -0,0 +301,4 @@case "ArrowUp":case "ArrowDown": {e.preventDefault();const ids = cardsIn(lane, cardId);app/components/TuxKanbanBoard.vue:304· MEDIUM —Math.random()used foraria-describedbyID, risking SSR hydration mismatchhintIdis generated withMath.random(), producing mismatched server/client IDs after hydration and breaking thearia-describedbylinkage for screen readers, while sibling components in the same PR correctly useuseId().Proposed replacement (one-click ⚡ Apply on the findings board at the top of this PR):
Evidence: strong evidence — two or more reviewers, quote verified against the diff, anchored on a changed line (panel 4/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
@ -0,0 +91,4 @@position: absolute;top: -6px;left: 0;right: 0;app/components/TuxKanbanCard.vue:94· LOW — Custom focus outline conflicts with the universal--shadow-focussystemThe
.tux-kanban-card:focus-visiblerule defines a custom maroon outline instead of using the project-wide focus ring (--shadow-focus), creating a redundant and visually inconsistent focus indicator; the v2.1.0 changelog specifies all components should inherit the universal two-ring focus style.Proposed replacement (one-click ⚡ Apply on the findings board at the top of this PR):
Evidence: strong evidence — two or more reviewers, quote verified against the diff, anchored on a changed line (panel 4/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
@ -0,0 +43,4 @@const titleId = `tux-kanban-lane-${props.laneId.replace(/[^\w-]/g, "_")}-${Math.random().toString(36).slice(2, 6)}`;</script><template>app/components/TuxKanbanLane.vue:46· MEDIUM —Math.random()used foraria-labelledbyID, risking SSR hydration mismatchtitleIdis built withMath.random(), which returns a different value on the server versus the client; because this component lives in Nuxt's auto-importapp/components/directory and may be prerendered, thearia-labelledbyID won't match after hydration and can break screen-reader label associations.Proposed replacement (one-click ⚡ Apply on the findings board at the top of this PR):
Evidence: strong evidence — two or more reviewers, quote verified against the diff, anchored on a changed line (panel 4/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
🔧 Security-gate fix map
The gate failed on these dependency findings — fastest path to green for each:
@nuxtjs/mdc@tiptap/corenpm update core --package-lock-onlyjs-yamlnpm update js-yaml --package-lock-onlysharpnpm update sharp --package-lock-onlysvgonpm update svgo --package-lock-onlyPosted once per head commit by the baseline gate (M2). A Renovate PR that only touches a manifest with no lockfile change is a broken pre-2026-08-06 artifact — check its diff before merging.
AI review · advisory
Verdict: 4 things worth fixing (4 medium).
Findings that didn't map to a diff line:
app/components/TuxMilestoneRoadmap.vue:0· MEDIUM — Row click handler firesonSelecteven when the row is a non-interactivediv, bypassing theinteractiveguard at the click levelThe row
<component :is="interactive ? 'button' : 'div'">has@click="onSelect(item)"unconditionally bound. WhileonSelectchecksprops.interactivebefore emitting, the click handler still fires the function on adivin read-only mode; the guard is inside the handler rather than at the DOM level. This is a minor inconsistency rather than a bug since no emit occurs, but it means keyboard users in read-only mode get no feedback and the click target is semantically inert. Low impact but worth noting for a read-only roadmap.⚑ panel: GPT-OSS 120B · Gemma 4 31B · Devstral 2 123B · Laguna S 2.1 — 15 distinct, 4 confirmed, 1 below threshold, 10 refuted · web: not used · context: 8 files under review · 89 codebase · 5 standards chunks (best-grounded: Gemma 4 31B; smaller windows saw less)
Panel debate — how this review was reached
Grounding — context: 8 files under review · 89 codebase · 5 standards chunks (best-grounded: Gemma 4 31B; smaller windows saw less)
Round 1 — independent reviews
Round 2 — cross-examination
Laguna S 2.1#3Button crumbs emit navigate on click but lack atypeattribute, yielding `role · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Devstral 2 123B · support 0Laguna S 2.1#1Pointer-drag ghost is only reparented to hostEl but never restored on non-comple · confirmed: Gemma 4 31B, Devstral 2 123B · refuted: GPT-OSS 120B · support 2Laguna S 2.1#4Row click handler firesonSelecteven when the row is a non-interactivediv, · confirmed: GPT-OSS 120B, Gemma 4 31B, Devstral 2 123B · refuted: — · support 3Laguna S 2.1#6Arrow-key focus navigation on isolated strip allows ArrowUp/ArrowDown to navigat · confirmed: GPT-OSS 120B, Gemma 4 31B, Devstral 2 123B · refuted: — · support 3Devstral 2 123B#4Missing validation fortagOffunction in template · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Devstral 2 123B#3Missing validation fortagOffunction in template · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Laguna S 2.1#5Pointer draghoverhit-test against the dragged card's own slot can land on th · confirmed: Gemma 4 31B, Devstral 2 123B · refuted: GPT-OSS 120B · support 2Laguna S 2.1#2Keyboard Escape path calls cancelKeyboard which checks mode !== 'keyboard' but t · confirmed: Gemma 4 31B, Devstral 2 123B · refuted: GPT-OSS 120B · support 2Devstral 2 123B#1Missing validation forcrumb.toandcrumb.href· confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Devstral 2 123B#2Missing validation fortagOffunction · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0GPT-OSS 120B#3Potential missing ARIA attributes for button navigation · confirmed: — · refuted: Gemma 4 31B, Devstral 2 123B, Laguna S 2.1 · support 0Devstral 2 123B#5Missing validation forbuttonstyling · confirmed: — · refuted: GPT-OSS 120B, Gemma 4 31B, Laguna S 2.1 · support 0Not posted (support < 2, or contested at a severity where one refutation vetoes)
Laguna S 2.1#1Pointer-drag ghost is only reparented to hostEl but never restored on non-comple (support 2, vetoed by a refutation)Raised but refuted (left out of the review above)
Laguna S 2.1#3Button crumbs emit navigate on click but lack atypeattribute, yieldingrole — The updated breadcrumb template bindstype: 'button'for button crumbs, so a defaulttype="submit"` is no longer possible.Devstral 2 123B#4Missing validation fortagOffunction in template —tagOfonly returns known tag strings ('NuxtLink', 'a', 'button'); no validation is required and the template uses it correctly.Devstral 2 123B#3Missing validation fortagOffunction in template — Same reasoning as above; the template safely usestagOfwithout additional validation.Devstral 2 123B#1Missing validation forcrumb.toandcrumb.href—toOfgracefully handles undefinedcrumb.to; explicit validation of non‑empty strings is unnecessary.Synthesis — Laguna S 2.1 wrote the final review from 4 confirmed findings (+1 below threshold) · promotion: support ≥ 2, and no refutation at high severity.
Transcript
rv-20260924164509-2eb4bb— full round outputs, web results, and model reasoning are viewable by anyone with access to this repository via the AI gateway.Advisory — never a merge gate. Findings are ordered by how well the panel's own evidence checks out, strongest first. React 👍/👎 on any inline comment to tell it whether it was worth flagging — that is the only feedback this system gets, and every threshold in it is tuned from those reactions. Transcript
rv-20260924164509-2eb4bb.@ -0,0 +633,4 @@}.tux-dependency-graph__banner-ref:hover {border-color: color-mix(in srgb, var(--color-error) 50%, transparent);app/components/TuxDependencyGraph.vue:636· MEDIUM — Arrow-key focus navigation on isolated strip allows ArrowUp/ArrowDown to navigate the list but theonStripKeydowntreats up/down and left/right identically, which is non-standard for a horizontal chip stripThe
onStripKeydownhandler maps ArrowLeft/ArrowUp to index-1 and ArrowRight/ArrowDown to index+1 for the isolated strip chips. For a horizontally-arranged chip list, the WAI-ARIA convention is that Left/Right move between items and Up/Down are reserved for within-item navigation or have no effect — mirroring ArrowRight/ArrowLeft is a common deviation, but here the handler treats them as identical, which can disorient keyboard users expecting vertical arrows to do nothing or move to a related control. The graph nodes usetuxNearestGraphNode(direction-aware), but the strip does not.Proposed replacement (one-click ⚡ Apply on the findings board at the top of this PR):
Evidence: strong evidence — two or more reviewers, quote verified against the diff, anchored on a changed line (panel 4/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
@ -0,0 +162,4 @@if ((y - r.top) / r.height <= 0.5) beforeCardId.value = slot.dataset.kanbanCard ?? null;else {const next = slot.nextElementSibling as HTMLElement | null;beforeCardId.value = next?.dataset?.kanbanCard ?? null;app/components/TuxKanbanBoard.vue:165· MEDIUM — Pointer draghoverhit-test against the dragged card's own slot can land on the ghost, causingbeforeCardIdto be set to the moving card's idWhen dragging by pointer,
hover()callsroot.elementFromPoint(x, y)which, because the absolutely-positioned ghost is appended tohostEland visually overlaps the card's origin, can return the ghost element itself. The code then walksel?.closest("[data-kanban-card]")— but the ghost is a clone of the card andcloneNode(true)preservesdata-kanban-cardattributes. The guardslot.dataset.kanbanCard === cardIdthen keepsbeforeCardIdunchanged rather than resetting it, but in theelsebranch (where the slot's card differs) the logic readsslot.dataset.kanbanCardwhich for a ghost-over-lane scenario is the dragged card's own id — the closest[data-kanban-card]ancestor of the ghost is the ghost itself (since the ghost is inhostEl, not in a lane). This meansslotcould be the ghost, andslot.dataset.kanbanCard === cardIdmatches, leaving stalebeforeCardIdfrom wherever the card came from, rather than resetting tonullas the!slotbranch intends.Evidence: partly verified — two or more reviewers, quote or anchor verified (panel 3/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
@ -0,0 +194,4 @@if (!armed) {if (Math.hypot(e.clientX - startX, e.clientY - startY) < ARM_DISTANCE) return;armed = true;fromLane = laneId;app/components/TuxKanbanBoard.vue:197· MEDIUM — Keyboard Escape path calls cancelKeyboard which checks mode !== 'keyboard' but the Tab handler calls cancelKeyboard without checking mode, leaving a stale mode for non-keyboard dragsThe
onCardKeyTabcase callscancelKeyboard()directly, butcancelKeyboard()early-returns whenmode.value !== 'keyboard'. During a pointer drag (mode.value === 'pointer'), pressing Tab while a card is lifted via keyboard is not possible, but if a pointer drag is in progress and the user tabs away,cancelKeyboardis a no-op and the pointer drag state (draggingId,overLaneId, etc.) is never reset — the board remains in a--draggingstate with no cleanup of the window-level pointer listeners added inbeginDrag.Evidence: partly verified — two or more reviewers, quote or anchor verified (panel 3/4).
👍 if this was worth flagging · 👎 if it was not — a reaction on this comment is the whole feedback loop.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.