返回 DeepSeek-Reasonix
PromptShelf.tsx
根目录 / desktop / frontend / src / components / PromptShelf.tsx
1 import type { ReactNode, RefObject } from "react";
2
3 export { PromptAction, PromptDescriptionDisclosure, PromptDescriptionToggle } from "./PromptAction";
4
5 export function PromptShelf({
6 className,
7 cardClassName,
8 titleId,
9 descriptionId,
10 title,
11 badges,
12 meta,
13 actions,
14 children,
15 crumbs,
16 note,
17 quickActions,
18 headerActions,
19 footer,
20 barRef,
21 role = "dialog",
22 decision = false,
23 actionsRole = "listbox",
24 cardCollapsible = false,
25 collapsed = false,
26 onToggleCollapse,
27 }: {
28 className?: string;
29 cardClassName?: string;
30 titleId: string;
31 descriptionId?: string;
32 title: ReactNode;
33 badges?: ReactNode;
34 meta?: ReactNode;
35 actions?: ReactNode;
36 children?: ReactNode;
37 crumbs?: ReactNode;
38 // Rendered between the actions grid and the quick actions; used for
39 // focus-following detail previews and similar footnotes.
40 note?: ReactNode;
41 quickActions?: ReactNode;
42 headerActions?: ReactNode;
43 // Sticky confirm bar for select-then-confirm decision surfaces.
44 footer?: ReactNode;
45 barRef?: RefObject<HTMLDivElement | null>;
46 role?: "dialog" | "region";
47 // Decision surfaces keep a vertical full-width option list and a fixed
48 // confirm footer; all preceding content shares one viewport-bounded scroll.
49 decision?: boolean;
50 // Select-then-confirm surfaces use listbox. Immediate actions are a group of
51 // buttons so assistive technology does not announce them as pending choices.
52 actionsRole?: "listbox" | "group";
53 // Whole-card click-to-collapse (used by the todo shelf): clicking anywhere
54 // on the card toggles collapsed; interactive children stop propagation.
55 cardCollapsible?: boolean;
56 collapsed?: boolean;
57 onToggleCollapse?: () => void;
58 }) {
59 return (
60 <div
61 className={[
62 "prompt-shelf",
63 "prompt-shelf--harness",
64 decision ? "prompt-shelf--decision" : "",
65 className ?? "",
66 ]
67 .filter(Boolean)
68 .join(" ")}
69 aria-live="polite"
70 >
71 <div
72 ref={barRef}
73 className={[
74 "prompt-shelf__card",
75 cardCollapsible ? "prompt-shelf__card--collapsible" : "",
76 cardCollapsible && collapsed ? "prompt-shelf__card--collapsed" : "",
77 cardClassName ?? "",
78 ]
79 .filter(Boolean)
80 .join(" ")}
81 role={role}
82 aria-modal={role === "dialog" ? "false" : undefined}
83 aria-labelledby={titleId}
84 aria-describedby={descriptionId}
85 tabIndex={cardCollapsible ? 0 : -1}
86 onClick={cardCollapsible ? (event) => {
87 const target = event.target as HTMLElement;
88 if (target.closest("button, input, textarea, select, a, [role='button'], [role='option'], [role='radio'], [role='checkbox']")) return;
89 onToggleCollapse?.();
90 } : undefined}
91 onKeyDown={cardCollapsible && onToggleCollapse ? (event) => {
92 const target = event.target as HTMLElement;
93 if (target.closest("button, input, textarea, select, a, [role='button'], [role='option'], [role='radio'], [role='checkbox']")) return;
94 if (event.key === "Enter" || event.key === " ") {
95 event.preventDefault();
96 onToggleCollapse();
97 }
98 } : undefined}
99 >
100 <div className="prompt-shelf__content">
101 <div className="prompt-shelf__header">
102 <div className="prompt-shelf__copy">
103 <div id={titleId} className="prompt-shelf__title">
104 <span className="prompt-shelf__heading">{title}</span>
105 {meta && <span className="prompt-shelf__meta">{meta}</span>}
106 {badges && <span className="prompt-shelf__badges">{badges}</span>}
107 </div>
108 </div>
109 {headerActions && (
110 <div className="prompt-shelf__header-actions" onClick={(event) => event.stopPropagation()}>
111 {headerActions}
112 </div>
113 )}
114 </div>
115 {crumbs}
116 {children && <div className="prompt-shelf__body">{children}</div>}
117 {actions && <div className="prompt-shelf__actions" role={actionsRole}>{actions}</div>}
118 {note && <div className="prompt-shelf__footnote">{note}</div>}
119 {quickActions && <div className="prompt-shelf__quick-actions">{quickActions}</div>}
120 </div>
121 {footer && <div className="prompt-shelf__footer">{footer}</div>}
122 </div>
123 </div>
124 );
125 }
126
127 export function PromptBadge({ children, tone }: { children: ReactNode; tone?: "default" | "amber" | "danger" }) {
128 return (
129 <span
130 className={[
131 "prompt-shelf__badge",
132 tone === "amber" ? " prompt-shelf__badge--amber" : "",
133 tone === "danger" ? " prompt-shelf__badge--danger" : "",
134 ].join("")}
135 >
136 {children}
137 </span>
138 );
139 }
140
141 export function PromptHeaderAction({
142 children,
143 onClick,
144 ariaLabel,
145 disabled = false,
146 }: {
147 children: ReactNode;
148 onClick: () => void;
149 ariaLabel?: string;
150 disabled?: boolean;
151 }) {
152 return (
153 <button
154 className="prompt-shelf__header-button"
155 type="button"
156 onClick={onClick}
157 aria-label={ariaLabel}
158 disabled={disabled}
159 >
160 {children}
161 </button>
162 );
163 }
164
165 export function DecisionConfirmBar({
166 hint,
167 confirmLabel,
168 onConfirm,
169 secondaryLabel,
170 onSecondary,
171 disabled = false,
172 confirmDisabled = false,
173 danger = false,
174 }: {
175 hint: ReactNode;
176 confirmLabel: ReactNode;
177 onConfirm: () => void;
178 secondaryLabel?: ReactNode;
179 onSecondary?: () => void;
180 disabled?: boolean;
181 confirmDisabled?: boolean;
182 danger?: boolean;
183 }) {
184 return (
185 <div className="decision-confirm-bar">
186 {secondaryLabel && onSecondary && (
187 <button
188 type="button"
189 className="btn btn--small decision-confirm-bar__secondary"
190 onClick={onSecondary}
191 disabled={disabled}
192 >
193 {secondaryLabel}
194 </button>
195 )}
196 <div className="decision-confirm-bar__hint">{hint}</div>
197 <button
198 type="button"
199 className={[
200 "btn btn--small decision-confirm-bar__confirm",
201 danger ? "btn--danger" : "btn--primary",
202 ].join(" ")}
203 onClick={onConfirm}
204 disabled={disabled || confirmDisabled}
205 >
206 {confirmLabel}
207 </button>
208 </div>
209 );
210 }
211
211 lines Plain Text