mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 16:08:04 +08:00
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:
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user