Sprint: debug, zichtbaarheid componenten (#165)

* feat(debug-store): Zustand store met hydration-flag voor debug-modus

* feat(status-bar): dev-only debug-toggle via geïsoleerde sub-component

* feat(globals.css): debug-mode overlay CSS voor data-debug-id elementen

* feat(shared): data-debug-id+label op navigatie-componenten

* feat(shared): data-debug-id+label op form/select-componenten

* feat(shared): data-debug-id+label op display-componenten
This commit is contained in:
Janpeter Visser 2026-05-08 08:55:43 +02:00 committed by GitHub
parent f7464db837
commit a16988b957
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 154 additions and 35 deletions

View file

@ -3,3 +3,25 @@
@plugin "@tailwindcss/typography";
@import "./styles/theme.css";
/* Debug-mode overlay (alleen actief wanneer body.debug-mode is gezet door dev-only toggle) */
body.debug-mode [data-debug-id] {
outline: 2px dashed var(--info);
outline-offset: 1px;
position: relative;
}
body.debug-mode [data-debug-id]:hover::after {
content: attr(data-debug-label);
position: absolute;
top: 0;
left: 0;
background: var(--info-container);
color: var(--info-container-foreground);
font-size: 10px;
line-height: 1.2;
padding: 2px 4px;
white-space: nowrap;
border-radius: 2px;
z-index: 9999;
pointer-events: none;
}

View file

@ -28,6 +28,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">
<DemoTooltip show={isDemo}>
<button
onClick={() => !isDemo && handleActivate()}
@ -37,5 +38,6 @@ export function ActivateProductButton({ productId, isDemo, redirectTo, label = '
{label}
</button>
</DemoTooltip>
</span>
)
}

View file

@ -24,5 +24,5 @@ export function AlertToast() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alert])
return null
return <span data-debug-id="alert-toast" data-debug-label="AlertToast — shared/alert-toast.tsx" hidden />
}

View file

@ -13,6 +13,8 @@ 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"
>
<defs>
<linearGradient id="s4m-bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">

View file

@ -9,6 +9,8 @@ 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"
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

@ -10,9 +10,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 <>{children}</>
if (!show) return <span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">{children}</span>
return (
<span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="inline-flex" />}>
@ -21,5 +22,6 @@ export function DemoTooltip({ show, children }: DemoTooltipProps) {
<TooltipContent>Niet beschikbaar in demo-modus</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
)
}

View file

@ -3,7 +3,11 @@
// Shows a warning banner on screens narrower than 1024px.
export function MinWidthBanner() {
return (
<div className="lg:hidden bg-warning/10 border-b border-warning/30 px-4 py-2 text-center text-xs text-warning">
<div
data-debug-id="min-width-banner"
data-debug-label="MinWidthBanner — shared/min-width-banner.tsx"
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.
</div>
)

View file

@ -111,7 +111,11 @@ export function NavBar({
}
return (
<header className="bg-surface-container-low border-b border-border h-14 flex items-center px-4 shrink-0">
<header
data-debug-id="nav-bar"
data-debug-label="NavBar — shared/nav-bar.tsx"
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">

View file

@ -27,6 +27,7 @@ export function NotificationsBell({ currentUserId, isDemo }: NotificationsBellPr
)
return (
<span data-debug-id="notifications-bell" data-debug-label="NotificationsBell — shared/notifications-bell.tsx">
<NotificationsSheet
currentUserId={currentUserId}
isDemo={isDemo}
@ -53,5 +54,6 @@ export function NotificationsBell({ currentUserId, isDemo }: NotificationsBellPr
</button>
}
/>
</span>
)
}

View file

@ -8,7 +8,11 @@ interface PanelNavBarProps {
export function PanelNavBar({ title, actions, className }: PanelNavBarProps) {
return (
<div className={cn('flex items-center justify-between px-4 py-2 border-b border-border bg-surface-container-low shrink-0', className)}>
<div
data-debug-id="panel-nav-bar"
data-debug-label="PanelNavBar — shared/panel-nav-bar.tsx"
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>}
</div>

View file

@ -26,6 +26,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">
<Select
value={value}
onValueChange={(v) => { if (v) onChange(v as PbiStatusApi) }}
@ -39,5 +40,6 @@ export function PbiStatusSelect({ value, onChange, className }: PbiStatusSelectP
<SelectItem value="done">Afgerond</SelectItem>
</SelectContent>
</Select>
</span>
)
}

View file

@ -25,6 +25,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">
<Select
value={String(value)}
onValueChange={(v) => { if (v) onChange(parseInt(v)) }}
@ -39,5 +40,6 @@ export function PrioritySelect({ value, onChange, className }: PrioritySelectPro
<SelectItem value="4">Laag</SelectItem>
</SelectContent>
</Select>
</span>
)
}

View file

@ -11,5 +11,5 @@ export function SetCurrentProduct({ id, name }: { id: string; name: string }) {
return () => clearCurrentProduct()
}, [id, name, setCurrentProduct, clearCurrentProduct])
return null
return <span data-debug-id="set-current-product" data-debug-label="SetCurrentProduct — shared/set-current-product.tsx" hidden />
}

View file

@ -68,6 +68,7 @@ export function SprintSwitcher({
if (sprints.length === 0) {
return (
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
<TooltipProvider>
<Tooltip>
<TooltipTrigger
@ -79,10 +80,12 @@ export function SprintSwitcher({
<TooltipContent>Maak een sprint aan vanuit de Product Backlog</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
)
}
return (
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
<DropdownMenu>
<DropdownMenuTrigger
disabled={isPending}
@ -152,5 +155,6 @@ export function SprintSwitcher({
)}
</DropdownMenuContent>
</DropdownMenu>
</span>
)
}

View file

@ -0,0 +1,31 @@
'use client'
import { useEffect } from 'react'
import { useDebugStore } from '@/stores/debug-store'
export function DebugToggle() {
const { debugMode, _hydrated, hydrate, toggleDebugMode } = useDebugStore()
useEffect(() => {
hydrate(localStorage.getItem('scrum4me:debug-mode') === 'true')
}, [hydrate])
useEffect(() => {
if (!_hydrated) return
localStorage.setItem('scrum4me:debug-mode', String(debugMode))
document.body.classList.toggle('debug-mode', debugMode)
}, [debugMode, _hydrated])
return (
<button
type="button"
onClick={toggleDebugMode}
aria-label="Debug-modus togglen"
aria-pressed={debugMode}
data-active={debugMode}
className="ml-2 cursor-pointer rounded px-1 text-xs opacity-40 transition-opacity hover:opacity-100 data-[active=true]:text-info"
>
{'{ }'}
</button>
)
}

View file

@ -1,3 +1,7 @@
'use client'
import { DebugToggle } from './status-bar-debug-toggle'
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE
? new Date(process.env.NEXT_PUBLIC_BUILD_DATE).toLocaleDateString('nl-NL', {
day: 'numeric',
@ -7,12 +11,17 @@ const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE
: '—'
const version = process.env.NEXT_PUBLIC_APP_VERSION ?? '0.0.0'
const isDev = process.env.NODE_ENV !== 'production'
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">
<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"
>
<span>© {new Date().getFullYear()} Scrum4Me</span>
<span>v{version} · gebouwd op {buildDate}</span>
<span>v{version} · gebouwd op {buildDate}{isDev && <DebugToggle />}</span>
</footer>
)
}

View file

@ -34,14 +34,22 @@ const TYPE_STYLES: Record<string, { bg: string; label: string; labelColor: strin
export function StoryLog({ logs, repoUrl }: StoryLogProps) {
if (logs.length === 0) {
return (
<p className="text-sm text-muted-foreground text-center py-4">
<p
data-debug-id="story-log"
data-debug-label="StoryLog — shared/story-log.tsx"
className="text-sm text-muted-foreground text-center py-4"
>
Nog geen activiteit. Gebruik de REST API om logs toe te voegen.
</p>
)
}
return (
<div className="space-y-3">
<div
data-debug-id="story-log"
data-debug-label="StoryLog — shared/story-log.tsx"
className="space-y-3"
>
{logs.map(log => {
const style = TYPE_STYLES[log.type] ?? TYPE_STYLES.IMPLEMENTATION_PLAN
const isTestResult = log.type === 'TEST_RESULT'

View file

@ -23,11 +23,13 @@ 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">
<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">
{initials}
</AvatarFallback>
</Avatar>
</span>
)
}

View file

@ -46,6 +46,7 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
}
return (
<span data-debug-id="user-menu" data-debug-label="UserMenu — shared/user-menu.tsx">
<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"
@ -137,5 +138,6 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</span>
)
}

15
stores/debug-store.ts Normal file
View file

@ -0,0 +1,15 @@
import { create } from 'zustand'
type DebugStore = {
debugMode: boolean
_hydrated: boolean
hydrate: (value: boolean) => void
toggleDebugMode: () => void
}
export const useDebugStore = create<DebugStore>((set, get) => ({
debugMode: false,
_hydrated: false,
hydrate: (v) => set({ debugMode: v, _hydrated: true }),
toggleDebugMode: () => set({ debugMode: !get().debugMode }),
}))