feat(web): add fullscreen expand mode to quadrant-matrix component

- Add expand button in header to open FullScreenModal
- Add numbered circles (1-4) to quadrant headers
- Add expanded prop to show full content without line-clamp
- Reorder grid layout: Q1 top-left, Q2 top-right, Q3 bottom-left, Q4 bottom-right
- Remove axis labels for cleaner design
This commit is contained in:
yyh
2026-01-16 17:16:13 +08:00
parent d62e16b9bb
commit a3f1220d23
4 changed files with 183 additions and 102 deletions

View File

@ -1,7 +1,10 @@
'use client'
import type { FC } from 'react'
import type { QuadrantData } from './types'
import { useMemo } from 'react'
import { RiExpandDiagonalLine } from '@remixicon/react'
import { useCallback, useMemo, useState } from 'react'
import ActionButton from '@/app/components/base/action-button'
import FullScreenModal from '@/app/components/base/fullscreen-modal'
import QuadrantCard from './quadrant-card'
import { isValidQuadrantData, QUADRANT_CONFIGS } from './types'
@ -10,6 +13,8 @@ type QuadrantMatrixProps = {
}
const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
const [isExpanded, setIsExpanded] = useState(false)
const parsedData = useMemo<QuadrantData | null>(() => {
try {
const trimmed = content.trim()
@ -25,6 +30,14 @@ const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
}
}, [content])
const handleExpand = useCallback(() => {
setIsExpanded(true)
}, [])
const handleClose = useCallback(() => {
setIsExpanded(false)
}, [])
if (!parsedData) {
return (
<div className="flex items-center justify-center rounded-xl bg-components-panel-bg-blur p-8">
@ -44,94 +57,120 @@ const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
+ parsedData.q3.length
+ parsedData.q4.length
// Shared grid content component
const renderGrid = (expanded: boolean) => (
<div className="grid grid-cols-2 gap-3">
{/* Row 1: Q1 (Do First), Q2 (Schedule) */}
<QuadrantCard
config={QUADRANT_CONFIGS.q1}
tasks={parsedData.q1}
expanded={expanded}
/>
<QuadrantCard
config={QUADRANT_CONFIGS.q2}
tasks={parsedData.q2}
expanded={expanded}
/>
{/* Row 2: Q3 (Delegate), Q4 (Don't Do) */}
<QuadrantCard
config={QUADRANT_CONFIGS.q3}
tasks={parsedData.q3}
expanded={expanded}
/>
<QuadrantCard
config={QUADRANT_CONFIGS.q4}
tasks={parsedData.q4}
expanded={expanded}
/>
</div>
)
return (
<div className="w-full overflow-hidden rounded-xl bg-components-panel-bg-blur p-4">
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<div>
<div className="system-md-semibold text-text-primary">
Eisenhower Matrix
<>
<div className="w-full overflow-hidden rounded-xl bg-components-panel-bg-blur p-4">
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<div>
<div className="system-md-semibold text-text-primary">
Eisenhower Matrix
</div>
<div className="text-xs text-text-tertiary">
{totalTasks}
{' '}
task
{totalTasks !== 1 ? 's' : ''}
{' '}
prioritized
</div>
</div>
<div className="text-xs text-text-tertiary">
{totalTasks}
{' '}
task
{totalTasks !== 1 ? 's' : ''}
{' '}
prioritized
</div>
</div>
{/* Legend */}
<div className="flex items-center gap-3 text-[11px] text-text-quaternary">
<span>
<span className="font-medium text-text-accent">I</span>
{' '}
= Importance
</span>
<span>
<span className="font-medium text-text-warning">U</span>
{' '}
= Urgency
</span>
</div>
</div>
{/* Axis Labels - Horizontal */}
<div className="mb-2 grid grid-cols-2 gap-3 pl-9">
<div className="text-center text-[11px] text-text-tertiary">
<span className="rounded bg-components-panel-on-panel-item-bg px-2 py-0.5">
Not Urgent
</span>
</div>
<div className="text-center text-[11px] text-text-warning">
<span className="rounded bg-state-warning-hover px-2 py-0.5">
Urgent
</span>
</div>
</div>
{/* Main Grid with Row Labels */}
<div className="flex gap-3">
{/* Row Labels - Vertical (rotated 90 degrees) */}
<div className="flex w-6 shrink-0 flex-col gap-3">
<div className="flex min-h-[200px] items-center justify-center">
<span className="-rotate-90 whitespace-nowrap rounded bg-state-accent-hover px-2 py-0.5 text-[11px] text-text-accent">
Important
</span>
</div>
<div className="flex min-h-[200px] items-center justify-center">
<span className="-rotate-90 whitespace-nowrap rounded bg-components-panel-on-panel-item-bg px-2 py-0.5 text-[11px] text-text-tertiary">
Not Important
</span>
{/* Legend + Expand Button */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 text-[11px] text-text-quaternary">
<span>
<span className="font-medium text-blue-600">I</span>
{' '}
= Importance
</span>
<span>
<span className="font-medium text-orange-500">U</span>
{' '}
= Urgency
</span>
</div>
<ActionButton onClick={handleExpand}>
<RiExpandDiagonalLine className="h-4 w-4" />
</ActionButton>
</div>
</div>
{/* 2x2 Grid */}
<div className="min-w-0 flex-1">
<div className="grid grid-cols-2 gap-3">
{/* Row 1: Important */}
<QuadrantCard
config={QUADRANT_CONFIGS.q2}
tasks={parsedData.q2}
/>
<QuadrantCard
config={QUADRANT_CONFIGS.q1}
tasks={parsedData.q1}
/>
{renderGrid(false)}
</div>
{/* Row 2: Not Important */}
<QuadrantCard
config={QUADRANT_CONFIGS.q4}
tasks={parsedData.q4}
/>
<QuadrantCard
config={QUADRANT_CONFIGS.q3}
tasks={parsedData.q3}
/>
{/* Fullscreen Modal */}
<FullScreenModal
open={isExpanded}
onClose={handleClose}
closable
>
<div className="flex h-full flex-col p-6">
{/* Modal Header */}
<div className="mb-6 flex items-center justify-between">
<div>
<div className="text-xl font-semibold text-text-primary">
Eisenhower Matrix
</div>
<div className="text-sm text-text-tertiary">
{totalTasks}
{' '}
task
{totalTasks !== 1 ? 's' : ''}
{' '}
prioritized
</div>
</div>
<div className="flex items-center gap-3 text-sm text-text-quaternary">
<span>
<span className="font-medium text-blue-600">I</span>
{' '}
= Importance
</span>
<span>
<span className="font-medium text-orange-500">U</span>
{' '}
= Urgency
</span>
</div>
</div>
{/* Expanded Grid */}
<div className="min-h-0 flex-1">
{renderGrid(true)}
</div>
</div>
</div>
</div>
</FullScreenModal>
</>
)
}

View File

@ -7,29 +7,42 @@ import TaskItem from './task-item'
type QuadrantCardProps = {
config: QuadrantConfig
tasks: Task[]
expanded?: boolean
maxDisplay?: number
}
const QuadrantCard: FC<QuadrantCardProps> = ({
config,
tasks,
expanded = false,
maxDisplay = 3,
}) => {
const { title, subtitle, bgClass, borderClass, titleClass } = config
const displayTasks = tasks.slice(0, maxDisplay)
const remainingCount = Math.max(0, tasks.length - maxDisplay)
const { number, title, subtitle, bgClass, borderClass, titleClass } = config
const displayLimit = expanded ? Infinity : maxDisplay
const displayTasks = tasks.slice(0, displayLimit)
const remainingCount = Math.max(0, tasks.length - displayLimit)
return (
<div
className={cn(
'flex min-h-[200px] min-w-0 flex-col rounded-xl border p-3',
'flex min-w-0 flex-col rounded-xl border p-3',
bgClass,
borderClass,
expanded ? 'min-h-[280px]' : 'min-h-[200px]',
)}
>
{/* Header */}
{/* Header with numbered circle */}
<div className="mb-2 shrink-0">
<div className="flex items-center gap-2">
{/* Numbered circle */}
<span className={cn(
'flex h-5 w-5 items-center justify-center rounded-full border text-xs font-semibold',
borderClass,
titleClass,
)}
>
{number}
</span>
<span className={cn('system-sm-semibold', titleClass)}>{title}</span>
{tasks.length > 0 && (
<span className="bg-components-badge-bg-gray rounded-full px-1.5 py-0.5 text-[10px] font-medium text-text-tertiary">
@ -40,12 +53,20 @@ const QuadrantCard: FC<QuadrantCardProps> = ({
<div className="text-[11px] text-text-tertiary">{subtitle}</div>
</div>
{/* Task List - scrollable area */}
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
{/* Task List */}
<div className={cn(
'flex min-h-0 flex-1 flex-col gap-2',
expanded && 'overflow-y-auto',
)}
>
{displayTasks.length > 0
? (
displayTasks.map((task, index) => (
<TaskItem key={`${task.name}-${index}`} task={task} />
<TaskItem
key={`${task.name}-${index}`}
task={task}
expanded={expanded}
/>
))
)
: (
@ -55,8 +76,8 @@ const QuadrantCard: FC<QuadrantCardProps> = ({
)}
</div>
{/* More indicator */}
{remainingCount > 0 && (
{/* More indicator (only in non-expanded mode) */}
{!expanded && remainingCount > 0 && (
<div className="mt-2 shrink-0 text-center text-[11px] text-text-tertiary">
+
{remainingCount}

View File

@ -5,17 +5,24 @@ import { cn } from '@/utils/classnames'
type TaskItemProps = {
task: Task
expanded?: boolean
showScores?: boolean
}
const TaskItem: FC<TaskItemProps> = ({ task, showScores = true }) => {
const TaskItem: FC<TaskItemProps> = ({ task, expanded = false, showScores = true }) => {
const { name, description, deadline, importance_score, urgency_score, action_advice } = task
return (
<div className="group min-w-0 rounded-lg bg-components-panel-bg p-2.5 shadow-xs transition-all hover:shadow-sm">
{/* Header: Task Name + Scores */}
<div className="flex items-start justify-between gap-2">
<div className="system-sm-medium min-w-0 flex-1 truncate text-text-primary" title={name}>
<div
className={cn(
'system-sm-medium min-w-0 flex-1 text-text-primary',
!expanded && 'truncate',
)}
title={name}
>
{name}
</div>
{showScores && (
@ -34,7 +41,11 @@ const TaskItem: FC<TaskItemProps> = ({ task, showScores = true }) => {
{/* Description */}
{description && (
<div className="mt-1 line-clamp-2 text-xs text-text-tertiary">
<div className={cn(
'mt-1 text-xs text-text-tertiary',
!expanded && 'line-clamp-2',
)}
>
{description}
</div>
)}
@ -42,11 +53,7 @@ const TaskItem: FC<TaskItemProps> = ({ task, showScores = true }) => {
{/* Deadline Badge */}
{deadline && (
<div className="mt-1.5">
<span className={cn(
'inline-flex items-center rounded px-1.5 py-0.5 text-[10px]',
'bg-components-badge-bg-gray text-text-tertiary',
)}
>
<span className="bg-components-badge-bg-gray inline-flex items-center rounded px-1.5 py-0.5 text-[10px] text-text-tertiary">
{deadline}
</span>
</div>
@ -54,8 +61,14 @@ const TaskItem: FC<TaskItemProps> = ({ task, showScores = true }) => {
{/* Action Advice */}
{action_advice && (
<div className="mt-2 overflow-hidden border-t border-divider-subtle pt-2">
<p className="line-clamp-2 text-xs italic text-text-quaternary" title={action_advice}>
<div className="mt-2 border-t border-divider-subtle pt-2">
<p
className={cn(
'text-xs italic text-text-quaternary',
!expanded && 'line-clamp-2',
)}
title={!expanded ? action_advice : undefined}
>
{action_advice}
</p>
</div>

View File

@ -15,11 +15,12 @@ export type QuadrantData = {
q1: Task[] // Urgent & Important - Do First
q2: Task[] // Not Urgent & Important - Schedule
q3: Task[] // Urgent & Not Important - Delegate
q4: Task[] // Not Urgent & Not Important - Eliminate
q4: Task[] // Not Urgent & Not Important - Don't Do
}
export type QuadrantConfig = {
key: 'q1' | 'q2' | 'q3' | 'q4'
number: number
title: string
subtitle: string
bgClass: string
@ -27,9 +28,13 @@ export type QuadrantConfig = {
titleClass: string
}
// Layout based on Eisenhower Matrix:
// Q1 (Do First) - top-left, Q2 (Schedule) - top-right
// Q3 (Delegate) - bottom-left, Q4 (Don't Do) - bottom-right
export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
q1: {
key: 'q1',
number: 1,
title: 'Do First',
subtitle: 'Urgent & Important',
bgClass: 'bg-state-destructive-hover',
@ -38,6 +43,7 @@ export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
},
q2: {
key: 'q2',
number: 2,
title: 'Schedule',
subtitle: 'Important & Not Urgent',
bgClass: 'bg-state-accent-hover',
@ -46,6 +52,7 @@ export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
},
q3: {
key: 'q3',
number: 3,
title: 'Delegate',
subtitle: 'Urgent & Not Important',
bgClass: 'bg-state-warning-hover',
@ -54,7 +61,8 @@ export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
},
q4: {
key: 'q4',
title: 'Eliminate',
number: 4,
title: 'Don\'t Do',
subtitle: 'Not Urgent & Not Important',
bgClass: 'bg-components-panel-on-panel-item-bg',
borderClass: 'border-divider-regular',