Sprint: Verbeteren debug mode (#179)

* feat(PBI-49): add debugProps helper + Vitest test

Adds lib/debug.ts with debugProps(id, component, file) that returns
data-debug-id and data-debug-label attrs in dev mode, empty object in
production. Adds __tests__/lib/debug.test.ts covering both modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(PBI-49): add debug-id pattern doc + CLAUDE.md reference

Adds docs/patterns/debug-id.md documenting the named-component boundary
rule (6 punten), helper-voorbeeld, skip-criteria en motivatie voor
handmatige pad-argumenten. Voegt verwijzing toe aan CLAUDE.md
patterns-tabel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): migrate 17 shared/ components to debugProps helper

Replace hardcoded data-debug-id + data-debug-label attribute pairs with
{...debugProps(id, component, file)} spread in all 17 components/shared/
files. Existing debug-ids preserved unchanged.

* feat(PBI-49): add debugProps to backlog/, sprint/, solo/ components

* feat(PBI-49): add debugProps to jobs/ + ideas/ components

* feat(PBI-49): add debugProps to products/ + settings/ + notifications/ components

* feat(PBI-49): add debugProps to admin/ + dashboard/ + dialogs/ + mobile/ + split-pane/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(PBI-49): use attr(data-debug-id) for debug tooltip in globals.css

* refactor(PBI-49): remove data-debug-label from debugProps helper + test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): strip unused component/file args from debugProps in shared/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to StatusBar, NavBar, PanelNavBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/sprint/*

- new-sprint-dialog: __submit on submit button
- sprint-backlog: __list on SprintBacklogLeft + SprintBacklogRight scroll areas
- sprint-board-client: root wrapper div (display:contents) + __drag-overlay
- sprint-header: __title on goal button, __dates on dates button, __actions on action cluster
- sprint-run-controls: root on controls div, __start/__cancel on action buttons; __blockers-dialog on dialog content
- start-sprint-button: root on trigger button, __dialog on dialog content, __submit on submit button
- sync-active-sprint-cookie: no debug-id (returns null, side-effect only), comment added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/backlog/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/ideas/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/dashboard/* + components/markdown.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to new-product-button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/solo/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-elements to nav-status-indicators

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/jobs/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/products/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/notifications/*

- answer-modal: __content (scroll area), __submit (footer)
- notifications-bridge: skip comment (bridge, non-rendering wrapper)
- notifications-realtime-mount: skip comment (returns null)
- notifications-sheet: __header, __items (questions list)
- push-toggle: __switch (button), __label (button text) on subscribed/unsubscribed states

* feat(PBI-49): add BEM sub-element data-debug-id to components/settings/*

- leave-product-button: root only (single-button component)
- min-quota-editor: __input (number input), __save (save button)
- profile-editor: __username (bio/short-description input), __save (submit)
- role-manager: __roles (checkbox list), __add (save button)
- token-manager: __tokens (active tokens list), __generate (create button)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to admin, auth, dialogs, entity-dialog, mobile, split-pane

* docs(PBI-49): add debug-labels BEM pattern doc + CLAUDE.md entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-09 22:46:29 +02:00 committed by GitHub
parent ce43f7720a
commit d292e445d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 600 additions and 218 deletions

View file

@ -12,6 +12,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { cancelJobAction, deleteJobAction } from '@/actions/admin/jobs'
import { debugProps } from '@/lib/debug'
type Job = {
id: string
@ -100,7 +101,7 @@ function JobRow({ job }: { job: Job }) {
function StatusTable({ jobs }: { jobs: Job[] }) {
return (
<Table>
<Table data-debug-id="admin-jobs-table__table">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
@ -171,7 +172,7 @@ function CostRow({ job }: { job: Job }) {
function CostsTable({ jobs }: { jobs: Job[] }) {
return (
<Table>
<Table data-debug-id="admin-jobs-table__table">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
@ -203,8 +204,8 @@ export function JobsTable({ jobs }: { jobs: Job[] }) {
const [view, setView] = useState<'status' | 'costs'>('status')
return (
<div className="space-y-3">
<div className="flex gap-1">
<div className="space-y-3" {...debugProps('admin-jobs-table', 'JobsTable', 'components/admin/jobs-table.tsx')}>
<div className="flex gap-1" data-debug-id="admin-jobs-table__header">
<Button
size="sm"
variant={view === 'status' ? 'default' : 'outline'}

View file

@ -21,6 +21,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { adminArchiveProductAction, adminDeleteProductAction } from '@/actions/admin/products'
import { debugProps } from '@/lib/debug'
type Product = {
id: string
@ -78,8 +79,8 @@ function DeleteDialog({ product }: { product: Product }) {
export function ProductsTable({ products }: { products: Product[] }) {
return (
<Table>
<TableHeader>
<Table {...debugProps('admin-products-table', 'ProductsTable', 'components/admin/products-table.tsx')}>
<TableHeader data-debug-id="admin-products-table__header">
<TableRow>
<TableHead>Naam</TableHead>
<TableHead>Eigenaar</TableHead>
@ -90,7 +91,7 @@ export function ProductsTable({ products }: { products: Product[] }) {
<TableHead className="text-right">Acties</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableBody data-debug-id="admin-products-table__table">
{products.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">

View file

@ -26,6 +26,7 @@ import {
updateUserRolesAction,
setMustResetPasswordAction,
} from '@/actions/admin/users'
import { debugProps } from '@/lib/debug'
type UserWithRoles = {
id: string
@ -190,8 +191,8 @@ export function UsersTable({
currentUserId: string
}) {
return (
<Table>
<TableHeader>
<Table {...debugProps('admin-users-table', 'UsersTable', 'components/admin/users-table.tsx')}>
<TableHeader data-debug-id="admin-users-table__header">
<TableRow>
<TableHead>Gebruiker</TableHead>
<TableHead>Email</TableHead>
@ -201,7 +202,7 @@ export function UsersTable({
<TableHead className="text-right">Acties</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableBody data-debug-id="admin-users-table__table">
{users.map(user => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.username}</TableCell>

View file

@ -4,13 +4,14 @@ import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { debugProps } from '@/lib/debug'
type ActionResult = { error: string | Record<string, string[]> } | undefined
function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus()
return (
<Button type="submit" className="w-full" disabled={pending}>
<Button type="submit" className="w-full" disabled={pending} data-debug-id="auth-form__submit">
{pending ? 'Even wachten…' : label}
</Button>
)
@ -34,7 +35,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
const errorMessage = getErrorMessage(state)
return (
<form action={formAction} className="space-y-4">
<form action={formAction} className="space-y-4" {...debugProps('auth-form', 'AuthForm', 'components/auth/auth-form.tsx')}>
<div className="space-y-2">
<label htmlFor="username" className="text-sm font-medium text-foreground">
Gebruikersnaam
@ -48,6 +49,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
minLength={3}
placeholder="jouw-naam"
className="bg-input-background border-border focus-visible:ring-primary"
data-debug-id="auth-form__username"
/>
</div>
@ -64,6 +66,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
minLength={8}
placeholder="••••••••"
className="bg-input-background border-border focus-visible:ring-primary"
data-debug-id="auth-form__password"
/>
</div>

View file

@ -3,6 +3,7 @@
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
import { CodeBadge } from '@/components/shared/code-badge'
import { debugProps } from '@/lib/debug'
export const PRIORITY_BORDER: Record<number, string> = {
1: 'border-l-4 border-l-priority-critical',
@ -38,9 +39,10 @@ export const BacklogCard = forwardRef<HTMLDivElement, BacklogCardProps>(function
className,
)}
{...rest}
{...debugProps('backlog-card', 'BacklogCard', 'components/backlog/backlog-card.tsx')}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm leading-snug line-clamp-2 flex-1">{title}</p>
<p className="text-sm leading-snug line-clamp-2 flex-1" {...debugProps('backlog-card__title')}>{title}</p>
{code && <CodeBadge code={code} className="shrink-0 mt-0.5" />}
</div>
{(badge || actions) && (

View file

@ -2,6 +2,7 @@
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { debugProps } from '@/lib/debug'
interface EmptyPanelProps {
title?: string
@ -15,8 +16,8 @@ interface EmptyPanelProps {
export function EmptyPanel({ title, message, action }: EmptyPanelProps) {
return (
<div className="p-8 text-center text-muted-foreground space-y-3">
{title && <p className="text-sm font-medium text-foreground">{title}</p>}
<div className="p-8 text-center text-muted-foreground space-y-3" {...debugProps('empty-panel', 'EmptyPanel', 'components/backlog/empty-panel.tsx')}>
{title && <p className="text-sm font-medium text-foreground" {...debugProps('empty-panel__title')}>{title}</p>}
<p className="text-sm">{message}</p>
{action && (
<DemoTooltip show={action.disabled ?? false}>
@ -25,6 +26,7 @@ export function EmptyPanel({ title, message, action }: EmptyPanelProps) {
variant="outline"
disabled={action.disabled}
onClick={action.disabled ? undefined : action.onClick}
{...debugProps('empty-panel__cta')}
>
{action.label}
</Button>

View file

@ -27,6 +27,7 @@ import {
} from '@/components/shared/entity-dialog-layout'
import { createPbiAction, updatePbiAction } from '@/actions/pbis'
import type { PbiStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
export interface PbiDialogPbi {
id: string
@ -120,6 +121,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('pbi-dialog', 'PbiDialog', 'components/backlog/pbi-dialog.tsx')}
>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">
@ -168,6 +170,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
disabled={isDemo}
aria-invalid={!!fieldError('title')}
className={fieldError('title') ? 'border-error' : ''}
{...debugProps('pbi-dialog__title')}
/>
{fieldError('title') && <p className="text-xs text-error">{fieldError('title')}</p>}
</div>
@ -207,7 +210,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
Annuleren
</Button>
<DemoTooltip show={isDemo}>
<Button type="submit" form="pbi-form" disabled={pending || isDemo}>
<Button type="submit" form="pbi-form" disabled={pending || isDemo} {...debugProps('pbi-dialog__submit')}>
{pending ? '…' : isEdit ? 'Opslaan' : 'Aanmaken'}
</Button>
</DemoTooltip>

View file

@ -31,6 +31,7 @@ import { useBacklogStore } from '@/stores/backlog-store'
import { deletePbiAction } from '@/actions/pbis'
import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
import { BacklogCard } from './backlog-card'
import { EmptyPanel } from './empty-panel'
@ -390,8 +391,8 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
const activePbi = activeDragId ? pbiMap[activeDragId] : null
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0">
<div className="flex flex-col h-full" {...debugProps('pbi-list', 'PbiList', 'components/backlog/pbi-list.tsx')}>
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0" {...debugProps('pbi-list__header')}>
{filterPriority !== 'all' && (
<button
onClick={() => setFilterPriority('all')}
@ -551,7 +552,7 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
items={filtered.map(p => p.id)}
strategy={verticalListSortingStrategy}
>
<div className="p-3 flex flex-col gap-2">
<div className="p-3 flex flex-col gap-2" {...debugProps('pbi-list__items')}>
{filtered.map(pbi => (
<SortablePbiRow
key={pbi.id}

View file

@ -38,6 +38,7 @@ import {
} from '@/components/shared/entity-dialog-layout'
import { createStoryAction, updateStoryAction, deleteStoryAction, getStoryLogsAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import type { Story } from './story-panel'
export type StoryDialogState =
@ -147,6 +148,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('story-dialog', 'StoryDialog', 'components/backlog/story-dialog.tsx')}
>
<div className={cn(entityDialogHeaderClasses, 'flex-col items-stretch gap-1')}>
<div className="flex items-start gap-2">
@ -219,6 +221,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
disabled={isDemo}
aria-invalid={!!fieldError('title')}
className={fieldError('title') ? 'border-error' : ''}
{...debugProps('story-dialog__title')}
/>
{fieldError('title') && <p className="text-xs text-error">{fieldError('title')}</p>}
</div>
@ -317,7 +320,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
Annuleren
</Button>
<DemoTooltip show={isDemo}>
<Button type="submit" form="story-form" disabled={pending || isDemo}>
<Button type="submit" form="story-form" disabled={pending || isDemo} {...debugProps('story-dialog__submit')}>
{pending ? '…' : isEdit ? 'Opslaan' : 'Aanmaken'}
</Button>
</DemoTooltip>

View file

@ -30,6 +30,7 @@ import { usePlannerStore } from '@/stores/planner-store'
import { useBacklogStore } from '@/stores/backlog-store'
import { reorderStoriesAction } from '@/actions/stories'
import { StoryDialog, type StoryDialogState } from './story-dialog'
import { debugProps } from '@/lib/debug'
import { BacklogCard } from './backlog-card'
import { EmptyPanel } from './empty-panel'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
@ -210,7 +211,7 @@ export function StoryPanel({ productId, isDemo }: StoryPanelProps) {
const hasActiveFilters = filterStatus !== null || filterPriority !== null
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...debugProps('story-panel', 'StoryPanel', 'components/backlog/story-panel.tsx')}>
<PanelNavBar
title="Stories"
actions={
@ -260,7 +261,7 @@ export function StoryPanel({ productId, isDemo }: StoryPanelProps) {
}
/>
<div className="flex-1 overflow-y-auto p-4">
<div className="flex-1 overflow-y-auto p-4" {...debugProps('story-panel__tasks')}>
{selectedPbiId === null ? (
<EmptyPanel message="Selecteer een PBI om de stories te bekijken." />
) : rawStories.length === 0 ? (

View file

@ -30,6 +30,7 @@ import { useSelectionStore } from '@/stores/selection-store'
import { useBacklogStore, type BacklogTask } from '@/stores/backlog-store'
import { reorderTasksAction } from '@/actions/tasks'
import { BacklogCard } from './backlog-card'
import { debugProps } from '@/lib/debug'
import { EmptyPanel } from './empty-panel'
import { cn } from '@/lib/utils'
@ -152,15 +153,18 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
if (!selectedStoryId) return
router.push(`${closePath}?newTask=1&storyId=${selectedStoryId}`)
}}
{...debugProps('task-panel__actions')}
>
+ Nieuwe taak
</Button>
</DemoTooltip>
)
const dp = debugProps('task-panel', 'TaskPanel', 'components/backlog/task-panel.tsx')
if (tasks === null) {
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...dp}>
<PanelNavBar title="Taken" actions={navActions} />
<EmptyPanel message="Selecteer een story om de taken te bekijken." />
</div>
@ -169,7 +173,7 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
if (tasks.length === 0) {
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...dp}>
<PanelNavBar title="Taken" actions={navActions} />
<EmptyPanel
message="Nog geen taken voor deze story."
@ -186,7 +190,7 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
const activeTask = activeDragId ? tasks.find((t) => t.id === activeDragId) : null
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...dp}>
<PanelNavBar title="Taken" actions={navActions} />
<div className="flex-1 overflow-y-auto p-3">
<DndContext

View file

@ -4,6 +4,7 @@ import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { ProductDialog } from '@/components/dialogs/product-dialog'
import { debugProps } from '@/lib/debug'
export function NewProductButton() {
const [open, setOpen] = useState(false)
@ -11,7 +12,9 @@ export function NewProductButton() {
return (
<>
<Button onClick={() => setOpen(true)}>+ Nieuw product</Button>
<div {...debugProps('new-product-button', 'NewProductButton', 'components/dashboard/new-product-button.tsx')}>
<Button onClick={() => setOpen(true)} data-debug-id="new-product-button__trigger">+ Nieuw product</Button>
</div>
<ProductDialog
mode="create"
open={open}

View file

@ -12,6 +12,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { restoreProductAction } from '@/actions/products'
import { setActiveProductAction } from '@/actions/active-product'
import { ProductDialog, type ProductDialogProduct } from '@/components/dialogs/product-dialog'
import { debugProps } from '@/lib/debug'
interface Product {
id: string
@ -53,7 +54,7 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
if (products.length === 0) {
return (
<div className="bg-surface-container-low rounded-xl border border-border p-12 text-center space-y-3">
<div className="bg-surface-container-low rounded-xl border border-border p-12 text-center space-y-3" {...debugProps('product-list', 'ProductList', 'components/dashboard/product-list.tsx')}>
<p className="text-muted-foreground">
{showArchived
? 'Geen gearchiveerde producten.'
@ -69,7 +70,7 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
}
return (
<div className="grid gap-3">
<div className="grid gap-3" {...debugProps('product-list', 'ProductList', 'components/dashboard/product-list.tsx')}>
{products.map(product => (
<div
key={product.id}
@ -77,8 +78,9 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
className={`group bg-surface-container-low border border-border rounded-xl p-4 transition-colors ${
showArchived ? 'opacity-60' : 'cursor-pointer hover:border-primary'
}`}
data-debug-id="product-list__items"
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start justify-between gap-4" data-debug-id="product-list__header">
<div className="min-w-0">
<div className="flex items-center gap-2">
{product.code && <CodeBadge code={product.code} />}

View file

@ -28,6 +28,7 @@ import {
import { productSchema, type ProductInput } from '@/lib/schemas/product'
import { createProductAction, updateProductAction } from '@/actions/products'
import { useProductsStore } from '@/stores/products-store'
import { debugProps } from '@/lib/debug'
export interface ProductDialogProduct {
id: string
@ -159,6 +160,7 @@ export function ProductDialog(props: Props) {
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('product-dialog', 'ProductDialog', 'components/dialogs/product-dialog.tsx')}
>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">
@ -170,6 +172,7 @@ export function ProductDialog(props: Props) {
id="product-form"
onSubmit={form.handleSubmit(onSubmit)}
className={entityDialogBodyClasses}
data-debug-id="product-dialog__content"
>
<div className="grid gap-1.5">
<label htmlFor="product-name" className="text-sm font-medium">
@ -294,7 +297,7 @@ export function ProductDialog(props: Props) {
Annuleren
</Button>
<DemoTooltip show={isDemo}>
<Button type="submit" form="product-form" disabled={isPending || isDemo}>
<Button type="submit" form="product-form" disabled={isPending || isDemo} data-debug-id="product-dialog__submit">
{isPending ? '…' : mode === 'edit' ? 'Opslaan' : 'Aanmaken'}
</Button>
</DemoTooltip>

View file

@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { debugProps } from '@/lib/debug'
import {
AlertDialog,
AlertDialogContent,
@ -33,7 +34,7 @@ export function DirtyCloseGuard({ isDirty, onConfirm, children }: DirtyCloseGuar
<>
{children(attemptClose)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent size="sm">
<AlertDialogContent size="sm" {...debugProps('dirty-close-guard', 'DirtyCloseGuard', 'components/entity-dialog/dirty-close-guard.tsx')}>
<AlertDialogHeader>
<AlertDialogTitle>Wijzigingen niet opgeslagen</AlertDialogTitle>
<AlertDialogDescription>
@ -41,11 +42,12 @@ export function DirtyCloseGuard({ isDirty, onConfirm, children }: DirtyCloseGuar
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOpen(false)}>
<AlertDialogCancel data-debug-id="dirty-close-guard__cancel" onClick={() => setOpen(false)}>
Blijven
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
data-debug-id="dirty-close-guard__confirm"
onClick={() => { setOpen(false); onConfirm() }}
>
Weggooien

View file

@ -9,6 +9,7 @@ import { Download } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { debugProps } from '@/lib/debug'
import { downloadIdeaMdAction } from '@/actions/ideas'
interface Props {
@ -47,6 +48,7 @@ export function DownloadMdButton({ ideaId, kind, hasContent }: Props) {
onClick={handleClick}
disabled={pending || !hasContent}
title={hasContent ? `Download ${kind}_md` : 'Geen content'}
{...debugProps('download-md-button', 'DownloadMdButton', 'components/ideas/download-md-button.tsx')}
>
<Download className="size-3.5 mr-1" />
.md

View file

@ -20,6 +20,7 @@ import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
import type { IdeaStatusApi } from '@/lib/idea-status'
import { isIdeaEditable } from '@/lib/idea-status'
import type { IdeaDto } from '@/lib/idea-dto'
import { debugProps } from '@/lib/debug'
import { updateIdeaAction, archiveIdeaAction, updateSecondaryProductsAction } from '@/actions/ideas'
import { IdeaRowActions } from '@/components/ideas/idea-row-actions'
import { IdeaMdEditor } from '@/components/ideas/idea-md-editor'
@ -132,7 +133,7 @@ export function IdeaDetailLayout({
const badge = getIdeaStatusBadge(API_TO_DB[idea.status])
return (
<div className="p-6 max-w-5xl mx-auto w-full space-y-6">
<div className="p-6 max-w-5xl mx-auto w-full space-y-6" {...debugProps('idea-detail-layout', 'IdeaDetailLayout', 'components/ideas/idea-detail-layout.tsx')}>
{/* Breadcrumb / back-link */}
<Link
href="/ideas"
@ -143,7 +144,7 @@ export function IdeaDetailLayout({
</Link>
{/* Header */}
<header className="flex flex-wrap items-start justify-between gap-4">
<header className="flex flex-wrap items-start justify-between gap-4" data-debug-id="idea-detail-layout__header">
<div className="space-y-1">
<p className="font-mono text-xs text-muted-foreground">{idea.code}</p>
<h1 className="text-2xl font-medium text-foreground">{idea.title}</h1>
@ -183,7 +184,7 @@ export function IdeaDetailLayout({
<IdeaPbiLinkCard idea={idea} isDemo={isDemo} />
{/* Tab-switcher */}
<nav className="border-b border-input flex gap-1">
<nav className="border-b border-input flex gap-1" data-debug-id="idea-detail-layout__main">
{([
{ key: 'idee' as TabKey, label: 'Idee', disabled: false, hasContent: true },
{ key: 'grill' as TabKey, label: 'Grill', disabled: !grill_md, hasContent: !!grill_md },

View file

@ -29,6 +29,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
import type { IdeaStatusApi } from '@/lib/idea-status'
import type { IdeaDto } from '@/lib/idea-dto'
import { debugProps } from '@/lib/debug'
import { createIdeaAction, archiveIdeaAction } from '@/actions/ideas'
import { IdeaRowActions } from '@/components/ideas/idea-row-actions'
@ -258,9 +259,9 @@ export function IdeaList({ ideas, products, isDemo }: IdeaListProps) {
}
return (
<div className="space-y-4">
<div className="space-y-4" {...debugProps('idea-list', 'IdeaList', 'components/ideas/idea-list.tsx')}>
{/* Top-bar: search + nieuw-knop */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex flex-wrap items-center gap-3" data-debug-id="idea-list__toolbar">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
@ -402,7 +403,7 @@ export function IdeaList({ ideas, products, isDemo }: IdeaListProps) {
: 'Geen ideeën die aan de filters voldoen.'}
</p>
) : (
<Table>
<Table data-debug-id="idea-list__items">
<TableHeader>
<TableRow>
<TableHead className="w-24"><SortHeader col="code" label="Code" sortKey={sortKey} sortDir={sortDir} onSort={handleSort} /></TableHead>

View file

@ -16,6 +16,7 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { debugProps } from '@/lib/debug'
import { parsePlanMd, type PlanParseError } from '@/lib/idea-plan-parser'
import { updateGrillMdAction, updatePlanMdAction } from '@/actions/ideas'
@ -112,7 +113,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
const dirty = value !== initialValue
return (
<div className="space-y-3">
<div className="space-y-3" {...debugProps('idea-md-editor', 'IdeaMdEditor', 'components/ideas/idea-md-editor.tsx')}>
{errors.length > 0 && (
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-3 space-y-1">
<p className="text-xs font-medium text-status-blocked">
@ -138,6 +139,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
onKeyDown={onKeyDown}
rows={24}
className="font-mono text-sm leading-relaxed"
data-debug-id="idea-md-editor__textarea"
placeholder={
kind === 'grill'
? '# Idee — ...\n## Scope\n...'
@ -159,6 +161,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
size="sm"
onClick={save}
disabled={!dirty || submitting || (errors.length > 0 && kind === 'plan')}
data-debug-id="idea-md-editor__save"
>
<Save className="size-3.5 mr-1" />
Opslaan

View file

@ -11,6 +11,7 @@ import { ExternalLink, Link2Off } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { debugProps } from '@/lib/debug'
import { relinkIdeaPlanAction } from '@/actions/ideas'
import type { IdeaDto } from '@/lib/idea-dto'
@ -27,9 +28,9 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
if (idea.pbi && idea.product_id) {
return (
<div className="rounded-md border border-status-done/30 bg-status-done/10 p-4 flex items-center gap-3">
<div className="rounded-md border border-status-done/30 bg-status-done/10 p-4 flex items-center gap-3" {...debugProps('idea-pbi-link-card', 'IdeaPbiLinkCard', 'components/ideas/idea-pbi-link-card.tsx')}>
<div className="flex-1">
<p className="text-xs uppercase tracking-wide text-status-done font-medium">
<p className="text-xs uppercase tracking-wide text-status-done font-medium" data-debug-id="idea-pbi-link-card__title">
Gepland
</p>
<p className="text-sm">
@ -37,6 +38,7 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
<Link
href={`/products/${idea.product_id}`}
className="font-medium text-status-done hover:underline inline-flex items-center gap-1"
data-debug-id="idea-pbi-link-card__link"
>
{idea.pbi.code} {idea.pbi.title}
<ExternalLink className="size-3" />
@ -62,7 +64,7 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
}
return (
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-4 space-y-2">
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-4 space-y-2" {...debugProps('idea-pbi-link-card', 'IdeaPbiLinkCard', 'components/ideas/idea-pbi-link-card.tsx')}>
<div className="flex items-center gap-2">
<Link2Off className="size-4 text-status-blocked" />
<p className="text-sm font-medium text-status-blocked">

View file

@ -36,6 +36,7 @@ import {
} from '@/components/ui/tooltip'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { useSoloStore } from '@/stores/solo-store'
import { debugProps } from '@/lib/debug'
import {
startGrillJobAction,
startMakePlanJobAction,
@ -134,7 +135,7 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
}
return (
<div className="flex items-center gap-1">
<div className="flex items-center gap-1" {...debugProps('idea-row-actions', 'IdeaRowActions', 'components/ideas/idea-row-actions.tsx')}>
{/* Bekijk PBI — alleen zichtbaar in PLANNED */}
{status === 'planned' && idea.pbi && idea.product_id && (
<Button
@ -148,24 +149,28 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
)}
{/* Grill Me */}
<ActionButton
label="Grill"
icon={<Flame className="size-3.5" />}
enabled={grillEnabled}
blockedReason={grillBlockedReason}
isDemo={isDemo}
onClick={() => runStart(startGrillJobAction)}
/>
<span data-debug-id="idea-row-actions__grill">
<ActionButton
label="Grill"
icon={<Flame className="size-3.5" />}
enabled={grillEnabled}
blockedReason={grillBlockedReason}
isDemo={isDemo}
onClick={() => runStart(startGrillJobAction)}
/>
</span>
{/* Make Plan */}
<ActionButton
label="Plan"
icon={<Sparkles className="size-3.5" />}
enabled={makePlanEnabled}
blockedReason={makePlanBlockedReason}
isDemo={isDemo}
onClick={() => runStart(startMakePlanJobAction)}
/>
<span data-debug-id="idea-row-actions__plan">
<ActionButton
label="Plan"
icon={<Sparkles className="size-3.5" />}
enabled={makePlanEnabled}
blockedReason={makePlanBlockedReason}
isDemo={isDemo}
onClick={() => runStart(startMakePlanJobAction)}
/>
</span>
{/* Materialiseer */}
<ActionButton
@ -217,6 +222,7 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
disabled={isDemo || pending}
aria-label="Archiveer idee"
title="Archiveer"
data-debug-id="idea-row-actions__delete"
>
<Archive className="size-4" />
</Button>

View file

@ -12,6 +12,7 @@ import { Badge } from '@/components/ui/badge'
import { StoryLog } from '@/components/shared/story-log'
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
import type { ClaudeJobStatusApi } from '@/lib/job-status'
import { debugProps } from '@/lib/debug'
import type { IdeaSyncData } from '@/app/(app)/ideas/[id]/sync-tab-server'
interface Props {
@ -88,9 +89,9 @@ export function IdeaSyncTab({ data }: Props) {
if (!pbi) return null
return (
<div className="space-y-4">
<div className="space-y-4" {...debugProps('idea-sync-tab', 'IdeaSyncTab', 'components/ideas/idea-sync-tab.tsx')}>
{/* Header: PBI-link + PR-status */}
<div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-surface-container p-3">
<div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-surface-container p-3" data-debug-id="idea-sync-tab__header">
<a
href={`/backlog/${pbi.id}`}
className="font-mono text-sm text-primary hover:underline"
@ -118,6 +119,7 @@ export function IdeaSyncTab({ data }: Props) {
</div>
{/* Stories */}
<div data-debug-id="idea-sync-tab__items">
{pbi.stories.length === 0 && (
<p className="text-sm text-muted-foreground italic">
Deze PBI heeft nog geen stories.
@ -228,6 +230,7 @@ export function IdeaSyncTab({ data }: Props) {
</div>
</details>
))}
</div>
</div>
)
}

View file

@ -25,6 +25,7 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { debugProps } from '@/lib/debug'
import { answerQuestion } from '@/actions/questions'
import { UserChatInput } from '@/components/ideas/user-chat-input'
@ -124,13 +125,13 @@ export function IdeaTimeline({
const showChatInput = planMd !== null
return (
<div className="space-y-4">
<div className="space-y-4" {...debugProps('idea-timeline', 'IdeaTimeline', 'components/ideas/idea-timeline.tsx')}>
{merged.length === 0 ? (
<p className="text-sm text-muted-foreground py-8 text-center italic">
Nog geen activiteit op dit idee.
</p>
) : (
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2">
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2" data-debug-id="idea-timeline__items">
{merged.map((entry, i) => {
// Expliciete locale + format om SSR/CSR hydration-mismatch te voorkomen
// (server-locale verschilde van browser-locale).

View file

@ -7,6 +7,7 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { debugProps } from '@/lib/debug'
import { createUserQuestionAction } from '@/actions/user-questions'
interface Props {
@ -39,7 +40,7 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
if (isDemo) {
return (
<div className="rounded-md border border-input bg-surface-container p-3">
<div className="rounded-md border border-input bg-surface-container p-3" {...debugProps('user-chat-input', 'UserChatInput', 'components/ideas/user-chat-input.tsx')}>
<p className="text-xs text-muted-foreground italic">
Demo-modus: vragen stellen is niet beschikbaar.
</p>
@ -48,7 +49,7 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
}
return (
<div className="space-y-2 rounded-md border border-input bg-surface-container p-3">
<div className="space-y-2 rounded-md border border-input bg-surface-container p-3" {...debugProps('user-chat-input', 'UserChatInput', 'components/ideas/user-chat-input.tsx')}>
<label className="text-xs font-medium text-muted-foreground">
Stel een vraag over dit plan
</label>
@ -58,12 +59,14 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
rows={3}
placeholder="Bijv. Waarom is gekozen voor X in plaats van Y?"
disabled={pending}
data-debug-id="user-chat-input__textarea"
/>
<div className="flex justify-end">
<Button
size="sm"
disabled={pending || !text.trim()}
onClick={submit}
data-debug-id="user-chat-input__submit"
>
<Send className="size-4" />
{pending ? 'Bezig…' : 'Verzend'}

View file

@ -1,6 +1,7 @@
'use client'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
import { jobStatusToApi } from '@/lib/job-status'
import type { ClaudeJobKind, ClaudeJobStatus } from '@prisma/client'
@ -61,8 +62,9 @@ export default function JobCard({
'border rounded-lg p-3 cursor-pointer hover:bg-surface-container transition-colors text-sm',
isSelected && 'ring-2 ring-primary',
)}
{...debugProps('job-card', 'JobCard', 'components/jobs/job-card.tsx')}
>
<div className="flex justify-between items-center gap-2">
<div className="flex justify-between items-center gap-2" data-debug-id="job-card__status">
<span className="text-[10px] px-1.5 py-0.5 rounded border bg-muted text-muted-foreground font-mono">
{KIND_LABELS[kind]}
</span>
@ -70,8 +72,8 @@ export default function JobCard({
{JOB_STATUS_LABELS[apiStatus]}
</span>
</div>
<p className="font-medium truncate mt-1">{titleText}</p>
<div className="flex items-end justify-between gap-2 mt-0.5">
<p className="font-medium truncate mt-1" data-debug-id="job-card__title">{titleText}</p>
<div className="flex items-end justify-between gap-2 mt-0.5" data-debug-id="job-card__actions">
<p className="text-xs text-muted-foreground truncate">{detailText}</p>
<span className="text-[10px] text-muted-foreground shrink-0 tabular-nums">
{new Date(createdAt).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })}

View file

@ -7,6 +7,7 @@ import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-st
import { jobStatusToApi } from '@/lib/job-status'
import type { JobWithRelations } from '@/actions/jobs-page'
import { Button } from '@/components/ui/button'
import { debugProps } from '@/lib/debug'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { restartClaudeJobAction } from '@/actions/claude-jobs'
@ -57,7 +58,7 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
if (!job) {
return (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
<div className="flex items-center justify-center h-full text-sm text-muted-foreground" {...debugProps('job-detail-pane', 'JobDetailPane', 'components/jobs/job-detail-pane.tsx')}>
Selecteer een job om details te zien
</div>
)
@ -75,7 +76,8 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
}
return (
<div className="overflow-y-auto h-full p-4">
<div className="overflow-y-auto h-full p-4" {...debugProps('job-detail-pane', 'JobDetailPane', 'components/jobs/job-detail-pane.tsx')}>
<div data-debug-id="job-detail-pane__header">
<FieldRow label="Status">
<span className={cn('text-xs px-2 py-0.5 rounded-full border font-medium', JOB_STATUS_COLORS[apiStatus])}>
{JOB_STATUS_LABELS[apiStatus]}
@ -118,7 +120,8 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
</pre>
) : '—'}
</FieldRow>
<div className="pt-3 mt-3 border-t border-border/50">
</div>
<div className="pt-3 mt-3 border-t border-border/50" data-debug-id="job-detail-pane__body">
<p className="text-xs text-muted-foreground mb-1.5">Beschrijving</p>
{job.description ? (
<pre className="text-xs whitespace-pre-wrap break-words bg-muted/40 rounded p-3 font-sans">

View file

@ -1,5 +1,6 @@
'use client'
import { debugProps } from '@/lib/debug'
import type { JobWithRelations } from '@/actions/jobs-page'
interface FieldRowProps {
@ -42,7 +43,7 @@ interface JobUsagePaneProps {
export default function JobUsagePane({ job }: JobUsagePaneProps) {
if (!job) {
return (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
<div className="flex items-center justify-center h-full text-sm text-muted-foreground" {...debugProps('job-usage-pane', 'JobUsagePane', 'components/jobs/job-usage-pane.tsx')}>
Selecteer een job om gebruik te zien
</div>
)
@ -57,15 +58,19 @@ export default function JobUsagePane({ job }: JobUsagePaneProps) {
const costLabel = job.costUsd != null ? `$${job.costUsd.toFixed(4)}` : '—'
return (
<div className="overflow-y-auto h-full p-4">
<FieldRow label="Model">{job.modelId ?? '—'}</FieldRow>
<FieldRow label="Tokens invoer">{formatNumber(job.inputTokens)}</FieldRow>
<FieldRow label="Tokens uitvoer">{formatNumber(job.outputTokens)}</FieldRow>
<FieldRow label="Cache read">{formatNumber(job.cacheReadTokens)}</FieldRow>
<FieldRow label="Cache write">{formatNumber(job.cacheWriteTokens)}</FieldRow>
<FieldRow label="Tokens totaal">{formatNumber(totalTokens || null)}</FieldRow>
<FieldRow label="Kosten (USD)">{costLabel}</FieldRow>
<FieldRow label="Duur">{formatDuration(job.startedAt, job.finishedAt)}</FieldRow>
<div className="overflow-y-auto h-full p-4" {...debugProps('job-usage-pane', 'JobUsagePane', 'components/jobs/job-usage-pane.tsx')}>
<div data-debug-id="job-usage-pane__header">
<FieldRow label="Model">{job.modelId ?? '—'}</FieldRow>
</div>
<div data-debug-id="job-usage-pane__table">
<FieldRow label="Tokens invoer">{formatNumber(job.inputTokens)}</FieldRow>
<FieldRow label="Tokens uitvoer">{formatNumber(job.outputTokens)}</FieldRow>
<FieldRow label="Cache read">{formatNumber(job.cacheReadTokens)}</FieldRow>
<FieldRow label="Cache write">{formatNumber(job.cacheWriteTokens)}</FieldRow>
<FieldRow label="Tokens totaal">{formatNumber(totalTokens || null)}</FieldRow>
<FieldRow label="Kosten (USD)">{costLabel}</FieldRow>
<FieldRow label="Duur">{formatDuration(job.startedAt, job.finishedAt)}</FieldRow>
</div>
</div>
)
}

View file

@ -7,6 +7,7 @@ import JobsColumn from './jobs-column'
import JobDetailPane from './job-detail-pane'
import JobUsagePane from './job-usage-pane'
import SprintSubTasksPane from './sprint-sub-tasks-pane'
import { debugProps } from '@/lib/debug'
import { useJobsStore } from '@/stores/jobs-store'
import useJobsRealtime from '@/hooks/use-jobs-realtime'
import type { ClaudeJobStatusApi } from '@/lib/job-status'
@ -61,7 +62,7 @@ export default function JobsBoard({ initialActiveJobs, initialDoneJobs, isDemo }
jobId={selectedJobId}
isSprintJob={selectedJob?.kind === 'SPRINT_IMPLEMENTATION'}
/>
<div className="flex gap-1 px-3 pt-3 pb-2 border-b shrink-0">
<div className="flex gap-1 px-3 pt-3 pb-2 border-b shrink-0" data-debug-id="jobs-board__toolbar">
<Button
size="sm"
variant={view === 'detail' ? 'default' : 'outline'}
@ -96,11 +97,15 @@ export default function JobsBoard({ initialActiveJobs, initialDoneJobs, isDemo }
)
return (
<SplitPane
panes={[leftPane, middlePane, rightPane]}
defaultSplit={[25, 50, 25]}
cookieKey="jobs"
tabLabels={['Actief', 'Details', 'Klaar']}
/>
<div className="h-full" {...debugProps('jobs-board', 'JobsBoard', 'components/jobs/jobs-board.tsx')}>
<div className="h-full" data-debug-id="jobs-board__columns">
<SplitPane
panes={[leftPane, middlePane, rightPane]}
defaultSplit={[25, 50, 25]}
cookieKey="jobs"
tabLabels={['Actief', 'Details', 'Klaar']}
/>
</div>
</div>
)
}

View file

@ -7,6 +7,7 @@ import JobCard from './job-card'
import { JOB_STATUS_LABELS } from '@/components/shared/job-status'
import { jobStatusToApi, type ClaudeJobStatusApi } from '@/lib/job-status'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import type { JobWithRelations } from '@/actions/jobs-page'
import type { ClaudeJobKind } from '@prisma/client'
@ -170,8 +171,8 @@ export default function JobsColumn({
const activeFilterCount = filterKinds.size + filterStatuses.size
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between gap-2 px-2 py-1.5 border-b border-border bg-surface-container-low shrink-0">
<div className="flex flex-col h-full" {...debugProps('jobs-column', 'JobsColumn', 'components/jobs/jobs-column.tsx')}>
<div className="flex items-center justify-between gap-2 px-2 py-1.5 border-b border-border bg-surface-container-low shrink-0" data-debug-id="jobs-column__header">
<span className="text-xs font-medium text-muted-foreground px-1">{title}</span>
<div className="flex items-center gap-1.5 flex-wrap justify-end">
{Array.from(filterKinds).map((k) => (
@ -240,7 +241,7 @@ export default function JobsColumn({
</Popover>
</div>
</div>
<div className="overflow-y-auto flex-1 p-2 space-y-2">
<div className="overflow-y-auto flex-1 p-2 space-y-2" data-debug-id="jobs-column__items">
{filtered.map((j) => (
<JobCard
key={j.id}

View file

@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
import type { ClaudeJobStatusApi } from '@/lib/job-status'
@ -44,7 +45,7 @@ function SubTaskList({ jobId }: { jobId: string }) {
if (subTasks.length === 0) return null
return (
<div className="border-b p-2 space-y-1 max-h-44 overflow-y-auto shrink-0">
<div className="border-b p-2 space-y-1 max-h-44 overflow-y-auto shrink-0" data-debug-id="sprint-sub-tasks-pane__items">
{subTasks.map(t => {
const apiStatus = t.status.toLowerCase() as ClaudeJobStatusApi
return (
@ -63,5 +64,9 @@ function SubTaskList({ jobId }: { jobId: string }) {
export default function SprintSubTasksPane({ jobId, isSprintJob }: SprintSubTasksPaneProps) {
if (!isSprintJob || !jobId) return null
return <SubTaskList key={jobId} jobId={jobId} />
return (
<div {...debugProps('sprint-sub-tasks-pane', 'SprintSubTasksPane', 'components/jobs/sprint-sub-tasks-pane.tsx')}>
<SubTaskList key={jobId} jobId={jobId} />
</div>
)
}

View file

@ -1,6 +1,7 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
interface MarkdownProps {
children: string
@ -9,7 +10,7 @@ interface MarkdownProps {
export function Markdown({ children, className }: MarkdownProps) {
return (
<div className={cn('prose prose-sm dark:prose-invert max-w-none', className)}>
<div className={cn('prose prose-sm dark:prose-invert max-w-none', className)} {...debugProps('markdown', 'Markdown', 'components/markdown.tsx')}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
disallowedElements={['script', 'iframe']}

View file

@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'
import { RotateCw } from 'lucide-react'
import { debugProps } from '@/lib/debug'
export function LandscapeGuard({ children }: { children: React.ReactNode }) {
const [isPortrait, setIsPortrait] = useState(false)
@ -22,9 +23,10 @@ export function LandscapeGuard({ children }: { children: React.ReactNode }) {
role="alert"
aria-live="assertive"
className="fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-background text-foreground p-6"
{...debugProps('landscape-guard', 'LandscapeGuard', 'components/mobile/landscape-guard.tsx')}
>
<RotateCw className="size-12 text-primary" />
<p className="text-base font-medium text-center">Draai je telefoon naar landscape</p>
<p className="text-base font-medium text-center" data-debug-id="landscape-guard__title">Draai je telefoon naar landscape</p>
</div>
)}
</>

View file

@ -14,6 +14,7 @@ import {
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { logoutAction } from '@/actions/auth'
import { debugProps } from '@/lib/debug'
export function LogoutButton() {
const [open, setOpen] = useState(false)
@ -31,6 +32,7 @@ export function LogoutButton() {
variant="outline"
onClick={() => setOpen(true)}
className="w-full justify-center gap-2"
{...debugProps('logout-button', 'LogoutButton', 'components/mobile/logout-button.tsx')}
>
<LogOut className="size-4" aria-hidden="true" />
Uitloggen

View file

@ -4,6 +4,7 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ListTree, Activity, Settings } from 'lucide-react'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
interface MobileTabBarProps {
activeProductId: string | null
@ -42,6 +43,7 @@ export function MobileTabBar({ activeProductId }: MobileTabBarProps) {
<nav
aria-label="Hoofdnavigatie"
className="fixed bottom-0 left-0 right-0 z-40 flex border-t border-border bg-surface-container-low"
{...debugProps('mobile-tab-bar', 'MobileTabBar', 'components/mobile/mobile-tab-bar.tsx')}
>
{tabs.map((tab) => {
const Icon = tab.icon
@ -52,6 +54,7 @@ export function MobileTabBar({ activeProductId }: MobileTabBarProps) {
href={tab.href}
aria-label={tab.label}
aria-current={active ? 'page' : undefined}
data-debug-id={`mobile-tab-bar__tab-${tab.label.toLowerCase()}`}
className={cn(
'flex-1 h-14 flex items-center justify-center transition-colors',
active

View file

@ -34,6 +34,7 @@ import {
import { ANSWER_MAX_CHARS } from '@/lib/schemas/question-answer'
import { answerQuestion } from '@/actions/questions'
import { useNotificationsStore, type NotificationQuestion } from '@/stores/notifications-store'
import { debugProps } from '@/lib/debug'
interface AnswerModalProps {
question: NotificationQuestion | null
@ -82,6 +83,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('answer-modal', 'AnswerModal', 'components/notifications/answer-modal.tsx')}
>
<div className={entityDialogHeaderClasses}>
<div className="flex flex-col gap-1">
@ -96,7 +98,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
</div>
</div>
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6">
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" data-debug-id="answer-modal__content">
<Link
href={
question.kind === 'idea'
@ -156,7 +158,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
)}
</div>
<div className={entityDialogFooterClasses}>
<div className={entityDialogFooterClasses} data-debug-id="answer-modal__submit">
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
Annuleren

View file

@ -106,5 +106,6 @@ export async function NotificationsBridge({ userId }: NotificationsBridgeProps)
const initial: NotificationQuestion[] = [...storyQuestions, ...ideaQuestions]
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
// debug-id: skip — bridge component (niet-renderende wrapper, zie docs/patterns/debug-id.md)
return <NotificationsRealtimeMount initial={initial} />
}

View file

@ -19,5 +19,6 @@ export function NotificationsRealtimeMount({ initial }: Props) {
}, [initial])
useNotificationsRealtime()
// debug-id: skip — render-loos mount (returns null, zie docs/patterns/debug-id.md)
return null
}

View file

@ -20,6 +20,7 @@ import { AnswerModal } from './answer-modal'
import { PushToggle } from './push-toggle'
import { cn } from '@/lib/utils'
import type { NotificationQuestion } from '@/stores/notifications-store'
import { debugProps } from '@/lib/debug'
interface NotificationsSheetProps {
trigger: React.ReactNode
@ -40,8 +41,8 @@ export function NotificationsSheet({
<>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger render={trigger as React.ReactElement} />
<SheetContent side="right" className="w-full sm:max-w-md">
<SheetHeader>
<SheetContent side="right" className="w-full sm:max-w-md" {...debugProps('notifications-sheet', 'NotificationsSheet', 'components/notifications/notifications-sheet.tsx')}>
<SheetHeader data-debug-id="notifications-sheet__header">
<SheetTitle>Vragen van Claude ({questions.length})</SheetTitle>
<SheetDescription>
Beantwoord open vragen om Claude verder te laten werken.
@ -53,7 +54,7 @@ export function NotificationsSheet({
Geen openstaande vragen. Lekker bezig!
</div>
) : (
<ul className="mt-4 flex flex-col gap-2 px-4 pb-4">
<ul className="mt-4 flex flex-col gap-2 px-4 pb-4" data-debug-id="notifications-sheet__items">
{questions.map((q) => {
// story-questions: forYou wanneer assignee = ingelogd; idee-vragen
// zijn altijd "voor jou" (idee is strikt user_id-only).

View file

@ -10,6 +10,7 @@ import {
subscribeToPush,
unsubscribeFromPush,
} from '@/lib/push-client'
import { debugProps } from '@/lib/debug'
type PushStatus =
| 'loading'
@ -85,7 +86,7 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
if (status === 'ios-needs-install') {
return (
<div className="rounded-lg bg-surface-variant p-3 text-sm text-on-surface-variant">
<div className="rounded-lg bg-surface-variant p-3 text-sm text-on-surface-variant" {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
Op iPhone/iPad: tik op het delen-icoon en kies{' '}
<strong>Zet op beginscherm</strong>. Daarna kun je notificaties activeren.
</div>
@ -94,7 +95,7 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
if (status === 'denied') {
return (
<p className="text-sm text-on-surface-variant">
<p className="text-sm text-on-surface-variant" {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
Notificaties zijn geblokkeerd. Schakel ze in via je browser-instellingen.
</p>
)
@ -102,15 +103,19 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
if (status === 'unsubscribed') {
return (
<Button variant="default" size="sm" onClick={handleSubscribe}>
Activeer push
</Button>
<div {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
<Button variant="default" size="sm" onClick={handleSubscribe} data-debug-id="push-toggle__switch">
<span data-debug-id="push-toggle__label">Activeer push</span>
</Button>
</div>
)
}
return (
<Button variant="outline" size="sm" onClick={handleUnsubscribe}>
Push uitzetten
</Button>
<div {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
<Button variant="outline" size="sm" onClick={handleUnsubscribe} data-debug-id="push-toggle__switch">
<span data-debug-id="push-toggle__label">Push uitzetten</span>
</Button>
</div>
)
}

View file

@ -3,6 +3,7 @@
import { useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { archiveProductAction } from '@/actions/products'
import { debugProps } from '@/lib/debug'
interface ArchiveProductButtonProps {
productId: string
@ -20,7 +21,7 @@ export function ArchiveProductButton({ productId }: ArchiveProductButtonProps) {
if (confirming) {
return (
<div className="flex gap-2 shrink-0">
<div className="flex gap-2 shrink-0" {...debugProps('archive-product-button', 'ArchiveProductButton', 'components/products/archive-product-button.tsx')}>
<Button
variant="destructive"
size="sm"
@ -47,6 +48,7 @@ export function ArchiveProductButton({ productId }: ArchiveProductButtonProps) {
size="sm"
className="shrink-0 border-error/40 text-error hover:bg-error/10"
onClick={() => setConfirming(true)}
{...debugProps('archive-product-button', 'ArchiveProductButton', 'components/products/archive-product-button.tsx')}
>
Archiveren
</Button>

View file

@ -4,6 +4,7 @@ import { useState, useTransition } from 'react'
import { cn } from '@/lib/utils'
import { updateAutoPrAction } from '@/actions/products'
import { toast } from 'sonner'
import { debugProps } from '@/lib/debug'
interface AutoPrToggleProps {
productId: string
@ -27,13 +28,14 @@ export function AutoPrToggle({ productId, initialValue }: AutoPrToggleProps) {
}
return (
<div className="flex items-center gap-3">
<div className="flex items-center gap-3" {...debugProps('auto-pr-toggle', 'AutoPrToggle', 'components/products/auto-pr-toggle.tsx')}>
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={handleToggle}
disabled={isPending}
data-debug-id="auto-pr-toggle__switch"
className={cn(
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent',
'transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
@ -49,7 +51,7 @@ export function AutoPrToggle({ productId, initialValue }: AutoPrToggleProps) {
)}
/>
</button>
<span className="text-sm text-foreground">Automatisch PR aanmaken na succesvolle agent-job</span>
<span className="text-sm text-foreground" data-debug-id="auto-pr-toggle__label">Automatisch PR aanmaken na succesvolle agent-job</span>
</div>
)
}

View file

@ -4,6 +4,7 @@ import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { ProductDialog, type ProductDialogProduct } from '@/components/dialogs/product-dialog'
import { debugProps } from '@/lib/debug'
interface Props {
product: ProductDialogProduct
@ -16,7 +17,7 @@ export function EditProductButton({ product, isDemo = false, size = 'sm', varian
const [open, setOpen] = useState(false)
return (
<>
<span {...debugProps('edit-product-button', 'EditProductButton', 'components/products/edit-product-button.tsx')}>
<DemoTooltip show={isDemo}>
<Button
variant={variant}
@ -34,6 +35,6 @@ export function EditProductButton({ product, isDemo = false, size = 'sm', varian
product={product}
isDemo={isDemo}
/>
</>
</span>
)
}

View file

@ -10,6 +10,7 @@ import {
SelectItem,
SelectTrigger,
} from '@/components/ui/select'
import { debugProps } from '@/lib/debug'
interface PrStrategySelectProps {
productId: string
@ -46,9 +47,9 @@ export function PrStrategySelect({ productId, initialValue }: PrStrategySelectPr
}
return (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2" {...debugProps('pr-strategy-select', 'PrStrategySelect', 'components/products/pr-strategy-select.tsx')}>
<Select value={value} onValueChange={handleChange} disabled={isPending}>
<SelectTrigger className="w-full max-w-xl">
<SelectTrigger className="w-full max-w-xl" data-debug-id="pr-strategy-select__trigger">
{STRATEGY_LABELS[value]}
</SelectTrigger>
<SelectContent>

View file

@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
type FieldErrors = Record<string, string[]>
type ActionResult = { error?: string | FieldErrors; success?: boolean } | undefined
@ -49,7 +50,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
const globalError = getGlobalError(state?.error)
return (
<form action={formAction} className="space-y-5">
<form action={formAction} className="space-y-5" {...debugProps('product-form', 'ProductForm', 'components/products/product-form.tsx')}>
{defaultValues?.id && (
<input type="hidden" name="id" value={defaultValues.id} />
)}
@ -82,6 +83,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
defaultValue={defaultValues?.name}
placeholder="bijv. DevPlanner"
className={fieldError('name') ? 'border-error' : ''}
data-debug-id="product-form__name"
/>
{fieldError('name') && (
<p className="text-xs text-error">{fieldError('name')}</p>
@ -117,6 +119,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
defaultValue={defaultValues?.repo_url ?? ''}
placeholder="https://github.com/..."
className={fieldError('repo_url') ? 'border-error' : ''}
data-debug-id="product-form__repo"
/>
{fieldError('repo_url') && (
<p className="text-xs text-error">{fieldError('repo_url')}</p>
@ -147,7 +150,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
</div>
)}
<div className="flex gap-3 pt-1">
<div className="flex gap-3 pt-1" data-debug-id="product-form__submit">
<SubmitButton label={submitLabel} />
</div>
</form>

View file

@ -4,6 +4,7 @@ import { useActionState, useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { addProductMemberAction, removeProductMemberAction } from '@/actions/products'
import { debugProps } from '@/lib/debug'
interface Member {
id: string
@ -29,11 +30,11 @@ export function TeamManager({ productId, members }: TeamManagerProps) {
}
return (
<div className="space-y-4">
<div className="space-y-4" {...debugProps('team-manager', 'TeamManager', 'components/products/team-manager.tsx')}>
{members.length === 0 ? (
<p className="text-sm text-muted-foreground">Nog geen teamleden toegevoegd.</p>
) : (
<ul className="space-y-2">
<ul className="space-y-2" data-debug-id="team-manager__members">
{members.map(m => (
<li key={m.id} className="flex items-center justify-between gap-3 rounded-lg bg-surface-container px-3 py-2">
<span className="text-sm font-medium text-foreground">{m.username}</span>
@ -51,7 +52,7 @@ export function TeamManager({ productId, members }: TeamManagerProps) {
</ul>
)}
<form action={formAction} className="flex gap-2">
<form action={formAction} className="flex gap-2" data-debug-id="team-manager__invite">
<input type="hidden" name="productId" value={productId} />
<Input
name="username"

View file

@ -4,6 +4,7 @@ import { useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { leaveProductAction } from '@/actions/products'
import { debugProps } from '@/lib/debug'
interface LeaveProductButtonProps {
productId: string
@ -22,7 +23,7 @@ export function LeaveProductButton({ productId, isDemo = false }: LeaveProductBu
if (confirming) {
return (
<div className="flex gap-2 shrink-0">
<div className="flex gap-2 shrink-0" {...debugProps('leave-product-button', 'LeaveProductButton', 'components/settings/leave-product-button.tsx')}>
<Button variant="destructive" size="sm" disabled={isPending} onClick={handleLeave}>
{isPending ? 'Bezig…' : 'Ja, verlaten'}
</Button>
@ -34,16 +35,18 @@ export function LeaveProductButton({ productId, isDemo = false }: LeaveProductBu
}
return (
<DemoTooltip show={isDemo}>
<Button
variant="outline"
size="sm"
className="shrink-0 border-error/40 text-error hover:bg-error/10"
disabled={isDemo}
onClick={() => !isDemo && setConfirming(true)}
>
Verlaten
</Button>
</DemoTooltip>
<span {...debugProps('leave-product-button', 'LeaveProductButton', 'components/settings/leave-product-button.tsx')}>
<DemoTooltip show={isDemo}>
<Button
variant="outline"
size="sm"
className="shrink-0 border-error/40 text-error hover:bg-error/10"
disabled={isDemo}
onClick={() => !isDemo && setConfirming(true)}
>
Verlaten
</Button>
</DemoTooltip>
</span>
)
}

View file

@ -5,6 +5,7 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { updateMinQuotaPctAction } from '@/actions/settings'
import { debugProps } from '@/lib/debug'
interface MinQuotaEditorProps {
currentValue: number
@ -27,7 +28,7 @@ export function MinQuotaEditor({ currentValue, isDemo }: MinQuotaEditorProps) {
}
return (
<div className="space-y-3">
<div className="space-y-3" {...debugProps('min-quota-editor', 'MinQuotaEditor', 'components/settings/min-quota-editor.tsx')}>
<div>
<label htmlFor="min-quota-pct" className="text-sm font-medium text-foreground">
Minimaal beschikbaar Claude-quota voordat de worker een job oppakt (%)
@ -46,10 +47,11 @@ export function MinQuotaEditor({ currentValue, isDemo }: MinQuotaEditorProps) {
onChange={e => setValue(Number(e.target.value))}
disabled={isDemo || isPending}
className="w-24 rounded border border-border bg-surface-container px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"
data-debug-id="min-quota-editor__input"
/>
<span className="text-sm text-muted-foreground">%</span>
<DemoTooltip show={isDemo}>
<Button onClick={handleSave} disabled={isDemo || isPending} size="sm">
<Button onClick={handleSave} disabled={isDemo || isPending} size="sm" data-debug-id="min-quota-editor__save">
Opslaan
</Button>
</DemoTooltip>

View file

@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { updateProfileAction } from '@/actions/profile'
import { debugProps } from '@/lib/debug'
interface ProfileEditorProps {
email: string | null
@ -63,7 +64,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
}
return (
<div className="space-y-5">
<div className="space-y-5" {...debugProps('profile-editor', 'ProfileEditor', 'components/settings/profile-editor.tsx')}>
<div className="flex items-center gap-5">
<button
type="button"
@ -137,6 +138,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
placeholder="Bijv. Full-stack developer bij Acme"
maxLength={160}
disabled={isPending}
data-debug-id="profile-editor__username"
/>
<p className="text-xs text-muted-foreground">Max. 160 tekens</p>
</div>
@ -158,7 +160,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
</div>
<div className="flex items-center gap-3">
<Button type="submit" size="sm" disabled={isPending}>
<Button type="submit" size="sm" disabled={isPending} data-debug-id="profile-editor__save">
{isPending ? 'Opslaan…' : 'Opslaan'}
</Button>
{state && 'success' in state && (

View file

@ -5,6 +5,7 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { updateRolesAction } from '@/actions/settings'
import { debugProps } from '@/lib/debug'
const ALL_ROLES = [
{ value: 'PRODUCT_OWNER', label: 'Product Owner' },
@ -46,9 +47,9 @@ export function RoleManager({ currentRoles, isDemo }: RoleManagerProps) {
}
return (
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4">
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4" {...debugProps('role-manager', 'RoleManager', 'components/settings/role-manager.tsx')}>
<h2 className="text-sm font-medium text-foreground">Mijn rollen</h2>
<div className="flex flex-wrap gap-3">
<div className="flex flex-wrap gap-3" data-debug-id="role-manager__roles">
{ALL_ROLES.map(role => (
<label key={role.value} className="flex items-center gap-2 cursor-pointer">
<input
@ -65,7 +66,7 @@ export function RoleManager({ currentRoles, isDemo }: RoleManagerProps) {
{error && <p className="text-xs text-error">{error}</p>}
{saved && <p className="text-xs text-success">Rollen opgeslagen.</p>}
<DemoTooltip show={isDemo}>
<Button size="sm" onClick={handleSave} disabled={isDemo}>Opslaan</Button>
<Button size="sm" onClick={handleSave} disabled={isDemo} data-debug-id="role-manager__add">Opslaan</Button>
</DemoTooltip>
</div>
)

View file

@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { createApiTokenAction, revokeApiTokenAction } from '@/actions/api-tokens'
import { debugProps } from '@/lib/debug'
interface Token {
id: string
@ -23,7 +24,7 @@ function CreateSubmitButton({ isDemo }: { isDemo: boolean }) {
const { pending } = useFormStatus()
return (
<DemoTooltip show={isDemo}>
<Button type="submit" disabled={isDemo || pending}>
<Button type="submit" disabled={isDemo || pending} data-debug-id="token-manager__generate">
{pending ? 'Aanmaken…' : 'Token aanmaken'}
</Button>
</DemoTooltip>
@ -63,7 +64,7 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
const revokedTokens = tokens.filter(t => t.revoked_at)
return (
<div className="space-y-6">
<div className="space-y-6" {...debugProps('token-manager', 'TokenManager', 'components/settings/token-manager.tsx')}>
{/* New token revealed */}
{newToken && (
<div className="bg-success-container border border-success/30 rounded-xl p-4 space-y-3">
@ -103,7 +104,7 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
{activeTokens.length === 0 ? (
<p className="text-sm text-muted-foreground">Geen actieve tokens.</p>
) : (
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border">
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border" data-debug-id="token-manager__tokens">
{activeTokens.map(token => (
<div key={token.id} className="flex items-center justify-between px-4 py-3 gap-3">
<div>

View file

@ -5,6 +5,7 @@ import { useTransition } from 'react'
import { toast } from 'sonner'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { setActiveProductAction } from '@/actions/active-product'
import { debugProps } from '@/lib/debug'
interface Props {
productId: string
@ -28,7 +29,7 @@ export function ActivateProductButton({ productId, isDemo, redirectTo, label = '
}
return (
<span data-debug-id="activate-product-button" data-debug-label="ActivateProductButton — shared/activate-product-button.tsx">
<span {...debugProps('activate-product-button')}>
<DemoTooltip show={isDemo}>
<button
onClick={() => !isDemo && handleActivate()}

View file

@ -3,6 +3,7 @@
import { useEffect } from 'react'
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
import { toast } from 'sonner'
import { debugProps } from '@/lib/debug'
const ALERT_MESSAGES: Record<string, string> = {
product_unavailable: 'Je actieve product is niet meer beschikbaar',
@ -24,5 +25,5 @@ export function AlertToast() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alert])
return <span data-debug-id="alert-toast" data-debug-label="AlertToast — shared/alert-toast.tsx" hidden />
return <span {...debugProps('alert-toast')} hidden />
}

View file

@ -1,3 +1,5 @@
import { debugProps } from '@/lib/debug'
interface AppIconProps {
size?: number
className?: string
@ -13,8 +15,7 @@ export function AppIcon({ size = 32, className }: AppIconProps) {
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-label="Scrum4Me"
data-debug-id="app-icon"
data-debug-label="AppIcon — shared/app-icon.tsx"
{...debugProps('app-icon')}
>
<defs>
<linearGradient id="s4m-bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">

View file

@ -1,4 +1,5 @@
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
interface CodeBadgeProps {
code: string | null | undefined
@ -9,8 +10,7 @@ export function CodeBadge({ code, className }: CodeBadgeProps) {
if (!code) return null
return (
<span
data-debug-id="code-badge"
data-debug-label="CodeBadge — shared/code-badge.tsx"
{...debugProps('code-badge')}
className={cn(
'inline-flex items-center rounded-md border border-border bg-surface-container px-1.5 py-0.5 font-mono text-[11px] leading-none text-muted-foreground',
className,

View file

@ -1,6 +1,7 @@
'use client'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { debugProps } from '@/lib/debug'
interface DemoTooltipProps {
show: boolean
@ -10,10 +11,10 @@ interface DemoTooltipProps {
// Wraps children with a "Niet beschikbaar in demo-modus" tooltip when show=true.
// Uses a span trigger so tooltip works on disabled elements.
export function DemoTooltip({ show, children }: DemoTooltipProps) {
if (!show) return <span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">{children}</span>
if (!show) return <span {...debugProps('demo-tooltip')}>{children}</span>
return (
<span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">
<span {...debugProps('demo-tooltip')}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="inline-flex" />}>

View file

@ -1,11 +1,12 @@
'use client'
import { debugProps } from '@/lib/debug'
// Shows a warning banner on screens narrower than 1024px.
export function MinWidthBanner() {
return (
<div
data-debug-id="min-width-banner"
data-debug-label="MinWidthBanner — shared/min-width-banner.tsx"
{...debugProps('min-width-banner')}
className="lg:hidden bg-warning/10 border-b border-warning/30 px-4 py-2 text-center text-xs text-warning"
>
Scrum4Me is ontworpen voor schermen van minimaal 1024px breed. Sommige functies zijn mogelijk niet goed bruikbaar op dit scherm.

View file

@ -20,6 +20,7 @@ import { NotificationsBell } from '@/components/shared/notifications-bell'
import { SoloNavStatusIndicators } from '@/components/solo/nav-status-indicators'
import { cn } from '@/lib/utils'
import { setActiveProductAction } from '@/actions/active-product'
import { debugProps } from '@/lib/debug'
interface NavBarProps {
isDemo: boolean
@ -112,13 +113,12 @@ export function NavBar({
return (
<header
data-debug-id="nav-bar"
data-debug-label="NavBar — shared/nav-bar.tsx"
{...debugProps('nav-bar')}
className="bg-surface-container-low border-b border-border h-14 flex items-center px-4 shrink-0"
>
{/* Links: logo + nav */}
<div className="flex items-center gap-4 flex-1">
<Link href="/" className="flex items-center gap-2 font-medium text-foreground">
<Link href="/" data-debug-id="nav-bar__app-icon" className="flex items-center gap-2 font-medium text-foreground">
<AppIcon size={24} />
<span className="text-primary font-semibold">Scrum4Me</span>
{isDemo && (
@ -157,6 +157,7 @@ export function NavBar({
<DropdownMenu>
<DropdownMenuTrigger
disabled={isPending}
data-debug-id="nav-bar__product-switcher"
className="flex items-center gap-1 text-sm font-medium text-foreground hover:text-primary transition-colors px-2 rounded-md hover:bg-surface-container focus:outline-none"
>
<span className="truncate max-w-[180px]">
@ -195,7 +196,9 @@ export function NavBar({
<div className="flex items-center gap-2 flex-1 justify-end">
<SoloNavStatusIndicators hasActiveProduct={!!activeProduct} minQuotaPct={minQuotaPct} />
<NotificationsBell currentUserId={userId} isDemo={isDemo} />
<UserMenu userId={userId} username={username} email={email} roles={roles} />
<span data-debug-id="nav-bar__user-menu">
<UserMenu userId={userId} username={username} email={email} roles={roles} />
</span>
</div>
</header>
)

View file

@ -11,6 +11,7 @@ import { Bell } from 'lucide-react'
import { useNotificationsStore } from '@/stores/notifications-store'
import { NotificationsSheet } from '@/components/notifications/notifications-sheet'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
interface NotificationsBellProps {
currentUserId: string
@ -27,7 +28,7 @@ export function NotificationsBell({ currentUserId, isDemo }: NotificationsBellPr
)
return (
<span data-debug-id="notifications-bell" data-debug-label="NotificationsBell — shared/notifications-bell.tsx">
<span {...debugProps('notifications-bell')}>
<NotificationsSheet
currentUserId={currentUserId}
isDemo={isDemo}

View file

@ -1,4 +1,5 @@
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
interface PanelNavBarProps {
title: string
@ -9,12 +10,11 @@ interface PanelNavBarProps {
export function PanelNavBar({ title, actions, className }: PanelNavBarProps) {
return (
<div
data-debug-id="panel-nav-bar"
data-debug-label="PanelNavBar — shared/panel-nav-bar.tsx"
{...debugProps('panel-nav-bar')}
className={cn('flex items-center justify-between px-4 py-2 border-b border-border bg-surface-container-low shrink-0', className)}
>
<span className="text-sm font-medium text-foreground">{title}</span>
{actions && <div className="flex items-center gap-2">{actions}</div>}
<span data-debug-id="panel-nav-bar__title" className="text-sm font-medium text-foreground">{title}</span>
{actions && <div data-debug-id="panel-nav-bar__actions" className="flex items-center gap-2">{actions}</div>}
</div>
)
}

View file

@ -3,6 +3,7 @@
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import type { PbiStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
export const PBI_STATUS_LABELS: Record<PbiStatusApi, string> = {
ready: 'Klaar voor sprint',
@ -26,7 +27,7 @@ interface PbiStatusSelectProps {
export function PbiStatusSelect({ value, onChange, className }: PbiStatusSelectProps) {
return (
<span data-debug-id="pbi-status-select" data-debug-label="PbiStatusSelect — shared/pbi-status-select.tsx">
<span {...debugProps('pbi-status-select')}>
<Select
value={value}
onValueChange={(v) => { if (v) onChange(v as PbiStatusApi) }}

View file

@ -2,6 +2,7 @@
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
export const PRIORITY_LABELS: Record<number, string> = {
1: 'Kritiek',
@ -25,7 +26,7 @@ interface PrioritySelectProps {
export function PrioritySelect({ value, onChange, className }: PrioritySelectProps) {
return (
<span data-debug-id="priority-select" data-debug-label="PrioritySelect — shared/priority-select.tsx">
<span {...debugProps('priority-select')}>
<Select
value={String(value)}
onValueChange={(v) => { if (v) onChange(parseInt(v)) }}

View file

@ -2,6 +2,7 @@
import { useEffect } from 'react'
import { useProductStore } from '@/stores/product-store'
import { debugProps } from '@/lib/debug'
export function SetCurrentProduct({ id, name }: { id: string; name: string }) {
const { setCurrentProduct, clearCurrentProduct } = useProductStore()
@ -11,5 +12,5 @@ export function SetCurrentProduct({ id, name }: { id: string; name: string }) {
return () => clearCurrentProduct()
}, [id, name, setCurrentProduct, clearCurrentProduct])
return <span data-debug-id="set-current-product" data-debug-label="SetCurrentProduct — shared/set-current-product.tsx" hidden />
return <span {...debugProps('set-current-product')} hidden />
}

View file

@ -15,6 +15,7 @@ import {
import { cn } from '@/lib/utils'
import { setActiveSprintAction } from '@/actions/active-sprint'
import type { SprintStatusApi } from '@/lib/task-status'
import { debugProps } from '@/lib/debug'
type SprintItem = { id: string; code: string; sprint_goal: string; status: SprintStatusApi }
@ -68,7 +69,7 @@ export function SprintSwitcher({
if (sprints.length === 0) {
return (
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
<span {...debugProps('sprint-switcher')}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
@ -85,7 +86,7 @@ export function SprintSwitcher({
}
return (
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
<span {...debugProps('sprint-switcher')}>
<DropdownMenu>
<DropdownMenuTrigger
disabled={isPending}

View file

@ -1,6 +1,7 @@
'use client'
import { DebugToggle } from './status-bar-debug-toggle'
import { debugProps } from '@/lib/debug'
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE
? new Date(process.env.NEXT_PUBLIC_BUILD_DATE).toLocaleDateString('nl-NL', {
@ -17,11 +18,10 @@ export function StatusBar() {
return (
<footer
className="shrink-0 border-t border-border bg-surface-container-low h-14 px-4 flex items-center justify-between text-sm text-muted-foreground select-none"
data-debug-id="status-bar"
data-debug-label="StatusBar — shared/status-bar.tsx"
{...debugProps('status-bar')}
>
<span>© {new Date().getFullYear()} Scrum4Me</span>
<span>v{version} · gebouwd op {buildDate}{isDev && <DebugToggle />}</span>
<span data-debug-id="status-bar__copyright">© {new Date().getFullYear()} Scrum4Me</span>
<span data-debug-id="status-bar__build-info">v{version} · gebouwd op {buildDate}{isDev && <DebugToggle />}</span>
</footer>
)
}

View file

@ -1,3 +1,5 @@
import { debugProps } from '@/lib/debug'
interface StoryLogEntry {
id: string
type: string
@ -35,8 +37,7 @@ export function StoryLog({ logs, repoUrl }: StoryLogProps) {
if (logs.length === 0) {
return (
<p
data-debug-id="story-log"
data-debug-label="StoryLog — shared/story-log.tsx"
{...debugProps('story-log')}
className="text-sm text-muted-foreground text-center py-4"
>
Nog geen activiteit. Gebruik de REST API om logs toe te voegen.
@ -46,8 +47,7 @@ export function StoryLog({ logs, repoUrl }: StoryLogProps) {
return (
<div
data-debug-id="story-log"
data-debug-label="StoryLog — shared/story-log.tsx"
{...debugProps('story-log')}
className="space-y-3"
>
{logs.map(log => {

View file

@ -2,6 +2,7 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
type AvatarSize = 'xs' | 'sm' | 'md' | 'lg'
@ -23,7 +24,7 @@ export function UserAvatar({ userId, username, size = 'md', className }: UserAva
const initials = username.slice(0, 2).toUpperCase()
return (
<span data-debug-id="user-avatar" data-debug-label="UserAvatar — shared/user-avatar.tsx">
<span {...debugProps('user-avatar')}>
<Avatar className={cn(SIZE_CLASSES[size], className)}>
<AvatarImage src={`/api/users/${userId}/avatar`} alt={username} />
<AvatarFallback className="bg-primary-container text-primary-container-foreground font-medium">

View file

@ -15,6 +15,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { debugProps } from '@/lib/debug'
const ROLE_LABELS: Record<string, string> = {
PRODUCT_OWNER: 'Product Owner',
@ -46,7 +47,7 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
}
return (
<span data-debug-id="user-menu" data-debug-label="UserMenu — shared/user-menu.tsx">
<span {...debugProps('user-menu')}>
<DropdownMenu>
<DropdownMenuTrigger
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"

View file

@ -8,6 +8,7 @@ import {
entityDialogFooterClasses,
entityDialogHeaderClasses,
} from '@/components/shared/entity-dialog-layout'
import { debugProps } from '@/lib/debug'
interface BatchEnqueueBlockerDialogProps {
open: boolean
@ -37,12 +38,12 @@ export function BatchEnqueueBlockerDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('batch-enqueue-blocker-dialog', 'BatchEnqueueBlockerDialog', 'components/solo/batch-enqueue-blocker-dialog.tsx')}>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">Blokkade gedetecteerd</DialogTitle>
</div>
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground">
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground" data-debug-id="batch-enqueue-blocker-dialog__content">
<p>
{BLOCKER_REASON_LABELS[blockerReason]}:{' '}
<span className="font-medium">{blockerLabel}</span>.
@ -60,7 +61,7 @@ export function BatchEnqueueBlockerDialog({
<div className={entityDialogFooterClasses}>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel}>
<Button variant="ghost" onClick={onCancel} data-debug-id="batch-enqueue-blocker-dialog__cancel">
Annuleer
</Button>
<TooltipProvider>
@ -71,6 +72,7 @@ export function BatchEnqueueBlockerDialog({
<Button
onClick={onConfirm}
disabled={noTasksBeforeBlocker}
data-debug-id="batch-enqueue-blocker-dialog__confirm"
>
{prefixCount === 1
? `Stuur ${prefixCount} taak tot aan blokkade`

View file

@ -4,6 +4,7 @@ import { useSoloStore } from '@/stores/solo-store'
import type { RealtimeStatus } from '@/stores/solo-store'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
function RealtimeIndicator({
status,
@ -63,12 +64,14 @@ export function SoloNavStatusIndicators({
workerQuotaPct < minQuotaPct
return (
<div className="flex items-center gap-3 px-2">
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<div className="flex items-center gap-3 px-2" {...debugProps('solo-nav-status-indicators', 'SoloNavStatusIndicators', 'components/solo/nav-status-indicators.tsx')}>
<span data-debug-id="nav-status-indicators__queue">
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
</span>
<div className="flex items-center gap-1 text-xs text-muted-foreground" data-debug-id="nav-status-indicators__running">
<span className={cn(
'size-2 rounded-full',
isStandby

View file

@ -1,4 +1,5 @@
import Link from 'next/link'
import { debugProps } from '@/lib/debug'
interface NoActiveSprintProps {
productId: string
@ -7,9 +8,9 @@ interface NoActiveSprintProps {
export function NoActiveSprint({ productId, productName }: NoActiveSprintProps) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6" {...debugProps('no-active-sprint', 'NoActiveSprint', 'components/solo/no-active-sprint.tsx')}>
<div className="text-4xl text-muted-foreground">🏃</div>
<h2 className="text-lg font-medium text-foreground">Geen actieve sprint</h2>
<h2 className="text-lg font-medium text-foreground" data-debug-id="no-active-sprint__title">Geen actieve sprint</h2>
<p className="text-sm text-muted-foreground max-w-sm">
Er is nog geen actieve sprint voor <span className="font-medium text-foreground">{productName}</span>.
Start een sprint in het Sprint Board om hier je taken te zien.
@ -17,6 +18,7 @@ export function NoActiveSprint({ productId, productName }: NoActiveSprintProps)
<Link
href={`/products/${productId}/sprint`}
className="text-sm text-primary hover:underline"
data-debug-id="no-active-sprint__cta"
>
Naar Sprint Board
</Link>

View file

@ -1,4 +1,5 @@
import Link from 'next/link'
import { debugProps } from '@/lib/debug'
interface Product {
id: string
@ -12,7 +13,7 @@ interface ProductPickerProps {
export function ProductPicker({ products }: ProductPickerProps) {
return (
<div className="p-6 max-w-2xl mx-auto w-full">
<div className="p-6 max-w-2xl mx-auto w-full" {...debugProps('product-picker', 'ProductPicker', 'components/solo/product-picker.tsx')}>
<h1 className="text-xl font-medium text-foreground mb-2">Solo bord</h1>
<p className="text-sm text-muted-foreground mb-6">
Kies een product om je persoonlijke Kanban-bord te openen.
@ -26,7 +27,7 @@ export function ProductPicker({ products }: ProductPickerProps) {
</Link>
</div>
) : (
<div className="grid gap-2">
<div className="grid gap-2" data-debug-id="product-picker__items">
{products.map(product => (
<Link
key={product.id}

View file

@ -11,6 +11,7 @@
import { useSoloRealtime } from '@/lib/realtime/use-solo-realtime'
// render-loos — geen JSX-root, data-debug-id niet van toepassing
export function SoloRealtimeBridge({ productId }: { productId: string | null }) {
useSoloRealtime(productId)
return null

View file

@ -10,6 +10,7 @@ import { useSoloStore } from '@/stores/solo-store'
import { taskStatusToApi } from '@/lib/task-status'
import { previewEnqueueAllAction, enqueueClaudeJobsBatchAction } from '@/actions/claude-jobs'
import { BatchEnqueueBlockerDialog } from './batch-enqueue-blocker-dialog'
import { debugProps } from '@/lib/debug'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { SplitPane } from '@/components/split-pane/split-pane'
@ -200,8 +201,8 @@ export function SoloBoard({
}
return (
<div className="flex flex-col h-full p-4 gap-4 min-h-0">
<div className="flex items-start justify-between gap-4 shrink-0">
<div className="flex flex-col h-full p-4 gap-4 min-h-0" {...debugProps('solo-board', 'SoloBoard', 'components/solo/solo-board.tsx')}>
<div className="flex items-start justify-between gap-4 shrink-0" data-debug-id="solo-board__header">
<div className="min-w-0 flex items-center gap-3">
<DemoTooltip show={isDemo}>
<Button
@ -231,7 +232,7 @@ export function SoloBoard({
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex-1 min-h-0">
<div className="flex-1 min-h-0" data-debug-id="solo-board__columns">
<SplitPane
cookieKey={`solo-${productId}`}
defaultSplit={[33, 33, 34]}

View file

@ -3,6 +3,7 @@
import { useDroppable } from '@dnd-kit/core'
import { cn } from '@/lib/utils'
import { SoloTaskCard } from './solo-task-card'
import { debugProps } from '@/lib/debug'
import type { SoloTask } from './solo-board'
export const COLUMN_CONFIG = {
@ -40,13 +41,14 @@ export function SoloColumn({ status, tasks, isDemo, onTaskClick }: SoloColumnPro
'flex flex-col h-full rounded-lg border border-border overflow-hidden',
isOver && 'ring-2 ring-primary ring-inset',
)}
{...debugProps('solo-column', 'SoloColumn', 'components/solo/solo-column.tsx')}
>
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)}>
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)} data-debug-id="solo-column__header">
<span className="text-sm font-medium">{config.label}</span>
<span className="text-xs opacity-60 ml-auto">{tasks.length}</span>
</div>
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]">
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]" data-debug-id="solo-column__tasks">
{tasks.map(task => (
<SoloTaskCard
key={task.id}

View file

@ -10,6 +10,7 @@ import { JOB_STATUS_LABELS, JOB_STATUS_COLORS, JOB_STATUS_ACTIVE } from '@/compo
import { useSoloStore } from '@/stores/solo-store'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import type { SoloTask } from './solo-board'
import { debugProps } from '@/lib/debug'
const PRIORITY_BORDER: Record<number, string> = {
1: 'border-l-4 border-l-priority-critical',
@ -47,9 +48,10 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
isDemo ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing',
)}
{...(!isDemo ? { ...attributes, ...listeners } : {})}
{...debugProps('solo-task-card', 'SoloTaskCard', 'components/solo/solo-task-card.tsx')}
>
{/* Regel 1: taaknaam + task_code */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-start justify-between gap-2" data-debug-id="solo-task-card__title">
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
{task.task_code && (
<TooltipProvider>
@ -106,7 +108,7 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
</div>
{/* Regel 4: story-info + job-badge */}
<div className="flex items-center justify-between gap-2 mt-0.5">
<div className="flex items-center justify-between gap-2 mt-0.5" data-debug-id="solo-task-card__status">
<p className="text-xs text-muted-foreground truncate flex-1">
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
{task.story_title}
@ -145,6 +147,7 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) {
'bg-surface-container rounded border border-primary px-3 py-2 shadow-xl opacity-90',
PRIORITY_BORDER[task.priority],
)}
{...debugProps('solo-task-card-overlay', 'SoloTaskCardOverlay', 'components/solo/solo-task-card.tsx')}
>
{/* Regel 1 */}
<div className="flex items-start justify-between gap-2">

View file

@ -19,6 +19,7 @@ import { useSoloStore } from '@/stores/solo-store'
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
import { cn } from '@/lib/utils'
import { getBranchUrl } from '@/lib/job-status-url'
import { debugProps } from '@/lib/debug'
import type { SoloTask } from './solo-board'
const STATUS_COLORS: Record<string, string> = {
@ -206,7 +207,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
</p>
</div>
<div className={entityDialogBodyClasses}>
<div className={entityDialogBodyClasses} data-debug-id="task-detail-dialog__content">
{task.description && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Beschrijving</p>
@ -380,7 +381,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
export function TaskDetailDialog({ task, productId, isDemo, repoUrl, onClose }: TaskDetailDialogProps) {
return (
<Dialog open={!!task} onOpenChange={(open) => { if (!open) onClose() }}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('task-detail-dialog', 'TaskDetailDialog', 'components/solo/task-detail-dialog.tsx')}>
{task && (
<TaskDetailContent
key={task.id}

View file

@ -10,6 +10,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { CodeBadge } from '@/components/shared/code-badge'
import { claimStoryAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
export interface UnassignedStoryTask {
id: string
@ -156,12 +157,12 @@ export function UnassignedStoriesSheet({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right">
<SheetContent side="right" {...debugProps('unassigned-stories-sheet', 'UnassignedStoriesSheet', 'components/solo/unassigned-stories-sheet.tsx')}>
<SheetHeader>
<SheetTitle>Openstaande stories</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto" data-debug-id="unassigned-stories-sheet__content">
{stories.length === 0 ? (
<div className="flex items-center justify-center h-32">
<p className="text-sm text-muted-foreground text-center">
@ -169,7 +170,7 @@ export function UnassignedStoriesSheet({
</p>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2" data-debug-id="unassigned-stories-sheet__items">
{stories.map(story => (
<ClaimStoryRow
key={story.id}

View file

@ -2,6 +2,7 @@
import { Fragment, useRef, useState, useEffect, useCallback } from 'react'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
const COOKIE_PREFIX = 'sp:'
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365
@ -123,7 +124,7 @@ export function SplitPane({
if (isMobile) {
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...debugProps('split-pane', 'SplitPane', 'components/split-pane/split-pane.tsx')}>
<div className="flex items-center border-b border-border shrink-0">
{activeTab > 0 && (
<button
@ -157,11 +158,12 @@ export function SplitPane({
}
return (
<div ref={containerRef} className="flex h-full overflow-hidden select-none">
<div ref={containerRef} className="flex h-full overflow-hidden select-none" {...debugProps('split-pane', 'SplitPane', 'components/split-pane/split-pane.tsx')}>
{panes.map((pane, i) => (
<Fragment key={i}>
{i > 0 && (
<div
data-debug-id="split-pane__divider"
onMouseDown={() => setDragging(i - 1)}
className={cn(
'w-1 shrink-0 bg-border hover:bg-primary transition-colors cursor-col-resize',
@ -172,6 +174,7 @@ export function SplitPane({
<div
className="flex flex-col overflow-hidden"
style={i === n - 1 ? { flex: 1 } : { width: `${splits[i]}%` }}
data-debug-id={i === 0 ? 'split-pane__left' : i === n - 1 ? 'split-pane__right' : undefined}
>
{pane}
</div>

View file

@ -21,6 +21,7 @@ import {
entityDialogHeaderClasses,
} from '@/components/shared/entity-dialog-layout'
import { createSprintWithPbisAction } from '@/actions/sprints'
import { debugProps } from '@/lib/debug'
interface NewSprintDialogProps {
open: boolean
@ -102,6 +103,7 @@ export function NewSprintDialog({
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('new-sprint-dialog', 'NewSprintDialog', 'components/sprint/new-sprint-dialog.tsx')}
>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">Nieuwe sprint</DialogTitle>
@ -173,6 +175,7 @@ export function NewSprintDialog({
type="submit"
form="new-sprint-form"
disabled={isPending || !sprintGoal.trim() || pbiIds.length === 0}
data-debug-id="new-sprint-dialog__submit"
>
{isPending ? 'Aanmaken…' : 'Sprint aanmaken'}
</Button>

View file

@ -26,6 +26,7 @@ import { PbiDialog, type PbiDialogState } from '@/components/backlog/pbi-dialog'
import { StoryDialog, type StoryDialogState } from '@/components/backlog/story-dialog'
import type { PbiStatusApi } from '@/lib/task-status'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
const STATUS_COLORS: Record<string, string> = {
OPEN: 'bg-status-todo/15 text-status-todo border-status-todo/30',
@ -276,7 +277,7 @@ export function SprintBacklogLeft({
const orderedStories = order.map(id => storyMap[id]).filter(Boolean)
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-left', 'SprintBacklogLeft', 'components/sprint/sprint-backlog.tsx')}>
<PanelNavBar
title="Sprint Backlog"
actions={
@ -293,6 +294,7 @@ export function SprintBacklogLeft({
/>
<div
ref={setNodeRef}
data-debug-id="sprint-backlog-left__list"
className={cn(
'flex-1 overflow-y-auto transition-colors',
isOver && 'bg-primary/5 ring-2 ring-inset ring-primary/20 rounded'
@ -631,10 +633,11 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, pr
)
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-right', 'SprintBacklogRight', 'components/sprint/sprint-backlog.tsx')}>
<PanelNavBar title="Product Backlog" actions={headerActions} />
<div
ref={setNodeRef}
data-debug-id="sprint-backlog-right__list"
className={cn(
'flex-1 overflow-y-auto py-2 transition-colors',
isOver && 'bg-error/5 ring-2 ring-inset ring-error/20 rounded'

View file

@ -18,6 +18,7 @@ import {
removeStoryFromSprintAction,
reorderSprintStoriesAction,
} from '@/actions/sprints'
import { debugProps } from '@/lib/debug'
interface SprintBoardClientProps {
productId: string
@ -193,6 +194,7 @@ export function SprintBoardClient({
const selectedTasks = selectedStoryId ? (tasksByStory[selectedStoryId] ?? []) : []
return (
<div {...debugProps('sprint-board-client')} className="contents">
<DndContext
id="sprint-board"
sensors={sensors}
@ -244,12 +246,13 @@ export function SprintBoardClient({
/>
<DragOverlay>
{activeDragStory && (
<div className="flex items-center gap-3 px-6 py-2 bg-popover border border-primary rounded shadow-lg text-sm opacity-95 w-72">
<div className="flex items-center gap-3 px-6 py-2 bg-popover border border-primary rounded shadow-lg text-sm opacity-95 w-72" data-debug-id="sprint-board-client__drag-overlay">
<span className="text-muted-foreground select-none"></span>
<span className="truncate flex-1">{activeDragStory.title}</span>
</div>
)}
</DragOverlay>
</DndContext>
</div>
)
}

View file

@ -32,6 +32,7 @@ import {
} from '@/components/shared/entity-dialog-layout'
import { updateSprintGoalAction, updateSprintDatesAction, completeSprintAction, setAllSprintTasksDoneAction } from '@/actions/sprints'
import type { SprintStory } from './sprint-backlog'
import { debugProps } from '@/lib/debug'
interface Sprint {
id: string
@ -130,7 +131,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
}
return (
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0">
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0" {...debugProps('sprint-header', 'SprintHeader', 'components/sprint/sprint-header.tsx')}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
@ -153,7 +154,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
</div>
</form>
) : (
<button onClick={() => !isDemo && setEditingGoal(true)} className="text-left mt-0.5 group">
<button onClick={() => !isDemo && setEditingGoal(true)} className="text-left mt-0.5 group" data-debug-id="sprint-header__title">
<p className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
{sprint.sprint_goal}
</p>
@ -161,9 +162,9 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex items-center gap-2 shrink-0" data-debug-id="sprint-header__actions">
<DemoTooltip show={isDemo}>
<Button size="sm" variant="ghost" disabled={isDemo} className="text-muted-foreground" onClick={() => !isDemo && setEditingDates(true)}>
<Button size="sm" variant="ghost" disabled={isDemo} className="text-muted-foreground" data-debug-id="sprint-header__dates" onClick={() => !isDemo && setEditingDates(true)}>
{sprint.start_date && sprint.end_date
? `${toDateInputValue(sprint.start_date)}${toDateInputValue(sprint.end_date)}`
: 'Datums instellen'}

View file

@ -174,13 +174,14 @@ export function SprintRunControls({
</Button>
</div>
)}
<div className="flex items-center gap-2">
<div className="flex items-center gap-2" data-debug-id="sprint-run-controls">
{canStart && (
<Button
size="sm"
onClick={handleStart}
disabled={pending || isDemo}
className="text-xs"
data-debug-id="sprint-run-controls__start"
>
Start Sprint
</Button>
@ -192,6 +193,7 @@ export function SprintRunControls({
disabled={pending || isDemo}
variant="default"
className="text-xs"
data-debug-id="sprint-run-controls__start"
>
Hervat sprint
</Button>
@ -203,6 +205,7 @@ export function SprintRunControls({
disabled={pending || isDemo}
variant="outline"
className="text-xs"
data-debug-id="sprint-run-controls__cancel"
>
Annuleer sprint-run
</Button>
@ -210,7 +213,7 @@ export function SprintRunControls({
</div>
<Dialog open={blockers !== null} onOpenChange={(open) => { if (!open) setBlockers(null) }}>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-lg" data-debug-id="sprint-run-controls__blockers-dialog">
<DialogHeader>
<DialogTitle>Sprint kan nog niet starten</DialogTitle>
<DialogDescription>

View file

@ -79,7 +79,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
return (
<>
<DemoTooltip show={isDemo}>
<Button size="sm" onClick={() => setOpen(true)} disabled={isDemo}>
<Button size="sm" onClick={() => setOpen(true)} disabled={isDemo} data-debug-id="start-sprint-button">
Sprint starten
</Button>
</DemoTooltip>
@ -89,6 +89,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
data-debug-id="start-sprint-button__dialog"
>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">Nieuwe Sprint starten</DialogTitle>
@ -171,7 +172,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
<Button type="button" variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
Annuleren
</Button>
<Button type="submit" form="start-sprint-form" disabled={pending}>
<Button type="submit" form="start-sprint-form" disabled={pending} data-debug-id="start-sprint-button__submit">
{pending ? 'Aanmaken…' : 'Sprint starten'}
</Button>
</div>

View file

@ -12,5 +12,6 @@ export function SyncActiveSprintCookie({ productId, sprintId }: Props) {
useEffect(() => {
syncActiveSprintCookieAction(productId, sprintId)
}, [productId, sprintId])
// No data-debug-id: this component renders null (side-effect only).
return null
}

View file

@ -21,6 +21,7 @@ import { PRIORITY_BORDER } from '@/components/backlog/backlog-card'
import { useSprintStore } from '@/stores/sprint-store'
import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { debugProps } from '@/lib/debug'
import { cn } from '@/lib/utils'
const STATUS_CYCLE: Record<string, 'TO_DO' | 'IN_PROGRESS' | 'DONE'> = {
@ -200,7 +201,7 @@ export function TaskList({ storyId, sprintId: _sprintId, productId: _productId,
}
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full" {...debugProps('task-list', 'TaskList', 'components/sprint/task-list.tsx')}>
<PanelNavBar
title="Taken"
actions={
@ -220,9 +221,9 @@ export function TaskList({ storyId, sprintId: _sprintId, productId: _productId,
}
/>
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto" data-debug-id="task-list__items">
{orderedTasks.length === 0 ? (
<div className="text-center mt-8 space-y-3">
<div className="text-center mt-8 space-y-3" data-debug-id="task-list__empty">
<p className="text-sm text-muted-foreground">Geen taken voor deze story.</p>
<DemoTooltip show={isDemo}>
<Button