@xenide-io/the-old-ui-theme 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-54ZR4VL5.mjs → chunk-RZXHZWUB.mjs} +44 -17
- package/dist/{index-B5NOyoKS.d.mts → index-Od4pIP4F.d.mts} +7 -1
- package/dist/{index-B5NOyoKS.d.ts → index-Od4pIP4F.d.ts} +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -10
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +152 -41
- package/dist/suite.d.ts +152 -41
- package/dist/suite.js +1184 -661
- package/dist/suite.mjs +1122 -600
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +37 -10
- package/dist/ui.mjs +1 -1
- package/package.json +7 -3
- package/src/components/ui/Modal.tsx +70 -26
- package/src/styles/suite-skin.css +284 -24
- package/src/suite/components/ai-panel.tsx +269 -103
- package/src/suite/components/suite-app-layout.tsx +48 -28
- package/src/suite/components/suite-layout.tsx +84 -40
- package/src/suite/components/suite-mobile-drawer.tsx +35 -22
- package/src/suite/components/suite-notification-bell.tsx +9 -3
- package/src/suite/components/suite-settings-layout.tsx +82 -0
- package/src/suite/components/suite-settings-mobile-nav.tsx +18 -17
- package/src/suite/components/suite-skeleton.tsx +3 -3
- package/src/suite/components/today-calibrating.tsx +11 -35
- package/src/suite/components/today-page-frame.tsx +19 -11
- package/src/suite/components/today-ui.tsx +276 -19
- package/src/suite/index.ts +41 -25
- package/src/suite/lib/apps.ts +32 -2
- package/src/suite/lib/use-sidebar-width.ts +114 -0
- package/src/components/sections/AccordionShowcase.tsx +0 -45
- package/src/components/sections/AlertShowcase.tsx +0 -40
- package/src/components/sections/AvatarShowcase.tsx +0 -80
- package/src/components/sections/BadgeShowcase.tsx +0 -65
- package/src/components/sections/ButtonShowcase.tsx +0 -308
- package/src/components/sections/CalendarShowcase.tsx +0 -35
- package/src/components/sections/CardShowcase.tsx +0 -159
- package/src/components/sections/CodeMockupShowcase.tsx +0 -56
- package/src/components/sections/ColorPalette.tsx +0 -117
- package/src/components/sections/CommandPaletteShowcase.tsx +0 -48
- package/src/components/sections/CountdownShowcase.tsx +0 -43
- package/src/components/sections/DiffShowcase.tsx +0 -70
- package/src/components/sections/DrawerShowcase.tsx +0 -97
- package/src/components/sections/DropdownShowcase.tsx +0 -98
- package/src/components/sections/EmptyStateShowcase.tsx +0 -50
- package/src/components/sections/FilterChipsShowcase.tsx +0 -98
- package/src/components/sections/FormShowcase.tsx +0 -127
- package/src/components/sections/HoverCardShowcase.tsx +0 -50
- package/src/components/sections/IconShowcase.tsx +0 -121
- package/src/components/sections/IndicatorShowcase.tsx +0 -52
- package/src/components/sections/KbdShowcase.tsx +0 -57
- package/src/components/sections/ModalShowcase.tsx +0 -211
- package/src/components/sections/NavigationShowcase.tsx +0 -199
- package/src/components/sections/NewComponentsShowcase.tsx +0 -1052
- package/src/components/sections/ProductLayoutsShowcase.tsx +0 -78
- package/src/components/sections/ProgressShowcase.tsx +0 -82
- package/src/components/sections/QuillChartShowcase.tsx +0 -92
- package/src/components/sections/QuillDashboardShowcase.tsx +0 -149
- package/src/components/sections/SegmentedControlShowcase.tsx +0 -49
- package/src/components/sections/SetupThemeShowcase.tsx +0 -262
- package/src/components/sections/SidebarShowcase.tsx +0 -152
- package/src/components/sections/StackShowcase.tsx +0 -33
- package/src/components/sections/StepperShowcase.tsx +0 -37
- package/src/components/sections/SuiteShowcase.tsx +0 -669
- package/src/components/sections/SwapShowcase.tsx +0 -99
- package/src/components/sections/TabShowcase.tsx +0 -84
- package/src/components/sections/TableShowcase.tsx +0 -142
- package/src/components/sections/TimelineShowcase.tsx +0 -83
- package/src/components/sections/ToastShowcase.tsx +0 -108
- package/src/components/sections/TooltipShowcase.tsx +0 -38
- package/src/components/sections/UtilityShowcase.tsx +0 -81
- package/src/styles/excel-themes.css +0 -110
- package/src/styles/lemon-primitives.css +0 -550
|
@@ -1,60 +1,157 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentType,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Check, Copy, Sparks, Xmark as X } from 'iconoir-react';
|
|
12
|
+
|
|
13
|
+
export const SUITE_OPEN_ASK_AI_EVENT = 'shellstack:open-ask-ai';
|
|
14
|
+
|
|
15
|
+
export type SuiteAskAiOpenDetail = {
|
|
16
|
+
prompt?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Open the suite Ask AI panel from anywhere (Today card, command palette, …). */
|
|
20
|
+
export function openSuiteAskAi(detail?: SuiteAskAiOpenDetail): void {
|
|
21
|
+
if (typeof window === 'undefined') return;
|
|
22
|
+
window.dispatchEvent(
|
|
23
|
+
new CustomEvent<SuiteAskAiOpenDetail>(SUITE_OPEN_ASK_AI_EVENT, {
|
|
24
|
+
detail: detail ?? {},
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
5
28
|
|
|
6
29
|
export interface SuiteAiPreset {
|
|
7
30
|
label: string;
|
|
8
31
|
prompt: string;
|
|
9
32
|
}
|
|
10
33
|
|
|
34
|
+
export interface SuiteAiChatMessage {
|
|
35
|
+
role: 'user' | 'assistant';
|
|
36
|
+
content: string;
|
|
37
|
+
created_at?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
/**
|
|
12
|
-
*
|
|
41
|
+
* Suite-wide Ask AI slide-over — Notion-style continuous chat.
|
|
13
42
|
*
|
|
14
|
-
* One
|
|
15
|
-
*
|
|
16
|
-
* its own floating launcher so it can be mounted once in the authenticated
|
|
17
|
-
* app shell.
|
|
43
|
+
* One thread per workspace (persisted via injected fetch/send/clear helpers).
|
|
44
|
+
* Not Kraken Deep Research. Apps mount this once in the authenticated shell.
|
|
18
45
|
*/
|
|
19
46
|
export function SuiteAiPanel({
|
|
20
|
-
presets,
|
|
21
|
-
emptyState,
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
presets = [],
|
|
48
|
+
emptyState = 'Ask anything about your workspace, or look something up online.',
|
|
49
|
+
title = 'Ask AI',
|
|
50
|
+
fetchChat,
|
|
51
|
+
sendMessage,
|
|
52
|
+
clearChat,
|
|
24
53
|
brandIcon: BrandIcon,
|
|
25
54
|
spinner: Spinner,
|
|
55
|
+
hideLauncher = false,
|
|
26
56
|
}: {
|
|
27
|
-
presets
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
presets?: SuiteAiPreset[];
|
|
58
|
+
emptyState?: ReactNode;
|
|
59
|
+
title?: string;
|
|
60
|
+
fetchChat: () => Promise<{
|
|
61
|
+
messages: SuiteAiChatMessage[];
|
|
62
|
+
configured: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
sendMessage: (params: {
|
|
65
|
+
prompt: string;
|
|
66
|
+
}) => Promise<{ messages: SuiteAiChatMessage[]; reply: string }>;
|
|
67
|
+
clearChat: () => Promise<void>;
|
|
68
|
+
brandIcon?: ComponentType<{ className?: string }>;
|
|
33
69
|
spinner: ComponentType<{ className?: string }>;
|
|
70
|
+
/** When true, only Today / events open the panel (no floating button). */
|
|
71
|
+
hideLauncher?: boolean;
|
|
34
72
|
}) {
|
|
35
73
|
const [open, setOpen] = useState(false);
|
|
36
74
|
const [prompt, setPrompt] = useState('');
|
|
75
|
+
const [messages, setMessages] = useState<SuiteAiChatMessage[]>([]);
|
|
37
76
|
const [loading, setLoading] = useState(false);
|
|
38
|
-
const [
|
|
77
|
+
const [hydrating, setHydrating] = useState(false);
|
|
39
78
|
const [error, setError] = useState('');
|
|
40
|
-
const [
|
|
79
|
+
const [copiedId, setCopiedId] = useState<number | null>(null);
|
|
80
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
81
|
+
const pendingPromptRef = useRef<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const scrollToBottom = useCallback(() => {
|
|
84
|
+
const el = scrollerRef.current;
|
|
85
|
+
if (!el) return;
|
|
86
|
+
el.scrollTop = el.scrollHeight;
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const hydrate = useCallback(async () => {
|
|
90
|
+
setHydrating(true);
|
|
91
|
+
setError('');
|
|
92
|
+
try {
|
|
93
|
+
const data = await fetchChat();
|
|
94
|
+
setMessages(data.messages ?? []);
|
|
95
|
+
if (!data.configured) {
|
|
96
|
+
setError('ShellStack AI is not configured for this workspace yet.');
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
setError(
|
|
100
|
+
err instanceof Error ? err.message : 'Could not load your Ask AI chat.',
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
setHydrating(false);
|
|
104
|
+
}
|
|
105
|
+
}, [fetchChat]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
function onOpen(event: Event) {
|
|
109
|
+
const detail = (event as CustomEvent<SuiteAskAiOpenDetail>).detail;
|
|
110
|
+
setOpen(true);
|
|
111
|
+
if (detail?.prompt?.trim()) {
|
|
112
|
+
pendingPromptRef.current = detail.prompt.trim();
|
|
113
|
+
setPrompt(detail.prompt.trim());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
window.addEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
|
|
117
|
+
return () => window.removeEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!open) return;
|
|
122
|
+
void hydrate().then(() => {
|
|
123
|
+
const pending = pendingPromptRef.current;
|
|
124
|
+
pendingPromptRef.current = null;
|
|
125
|
+
if (pending) {
|
|
126
|
+
// Leave prompt filled; user can send, or auto-send on next tick.
|
|
127
|
+
setPrompt(pending);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}, [open, hydrate]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (open) scrollToBottom();
|
|
134
|
+
}, [messages, loading, open, scrollToBottom]);
|
|
41
135
|
|
|
42
136
|
async function ask(text: string) {
|
|
43
137
|
const trimmed = text.trim();
|
|
44
138
|
if (!trimmed || loading) return;
|
|
45
139
|
setLoading(true);
|
|
46
140
|
setError('');
|
|
47
|
-
|
|
48
|
-
|
|
141
|
+
setPrompt('');
|
|
142
|
+
// Optimistic user bubble — replaced by server messages on success.
|
|
143
|
+
setMessages((prev) => [...prev, { role: 'user', content: trimmed }]);
|
|
49
144
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
setError('ShellStack AI is not configured for this workspace yet.');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const result = await complete({ prompt: trimmed });
|
|
56
|
-
setAnswer(result.text);
|
|
145
|
+
const result = await sendMessage({ prompt: trimmed });
|
|
146
|
+
setMessages(result.messages ?? []);
|
|
57
147
|
} catch (err) {
|
|
148
|
+
setMessages((prev) =>
|
|
149
|
+
prev[prev.length - 1]?.role === 'user' &&
|
|
150
|
+
prev[prev.length - 1]?.content === trimmed
|
|
151
|
+
? prev.slice(0, -1)
|
|
152
|
+
: prev,
|
|
153
|
+
);
|
|
154
|
+
setPrompt(trimmed);
|
|
58
155
|
setError(
|
|
59
156
|
err instanceof Error
|
|
60
157
|
? err.message
|
|
@@ -65,107 +162,174 @@ export function SuiteAiPanel({
|
|
|
65
162
|
}
|
|
66
163
|
}
|
|
67
164
|
|
|
68
|
-
async function
|
|
69
|
-
if (
|
|
165
|
+
async function handleClear() {
|
|
166
|
+
if (loading) return;
|
|
167
|
+
try {
|
|
168
|
+
await clearChat();
|
|
169
|
+
setMessages([]);
|
|
170
|
+
setError('');
|
|
171
|
+
} catch (err) {
|
|
172
|
+
setError(
|
|
173
|
+
err instanceof Error ? err.message : 'Could not clear this chat.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function copyMessage(index: number, content: string) {
|
|
70
179
|
try {
|
|
71
|
-
await navigator.clipboard.writeText(
|
|
72
|
-
|
|
73
|
-
window.setTimeout(() =>
|
|
180
|
+
await navigator.clipboard.writeText(content);
|
|
181
|
+
setCopiedId(index);
|
|
182
|
+
window.setTimeout(() => setCopiedId(null), 1500);
|
|
74
183
|
} catch {
|
|
75
|
-
// Clipboard unavailable
|
|
184
|
+
// Clipboard unavailable.
|
|
76
185
|
}
|
|
77
186
|
}
|
|
78
187
|
|
|
188
|
+
const Icon = BrandIcon ?? Sparks;
|
|
189
|
+
|
|
79
190
|
return (
|
|
80
191
|
<>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
192
|
+
{!hideLauncher ? (
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
data-test="ai-panel-launcher"
|
|
196
|
+
onClick={() => setOpen(true)}
|
|
197
|
+
className="fixed bottom-5 right-5 z-30 flex items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-4 py-2.5 text-sm font-medium text-ph-ink shadow-ph hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
198
|
+
aria-label="Open Ask AI"
|
|
199
|
+
>
|
|
200
|
+
<Icon className="h-4 w-4 text-ph-brand" />
|
|
201
|
+
Ask AI
|
|
202
|
+
</button>
|
|
203
|
+
) : null}
|
|
204
|
+
|
|
205
|
+
{open ? (
|
|
93
206
|
<div className="fixed inset-0 z-40" role="dialog" aria-modal="true">
|
|
94
207
|
<button
|
|
95
208
|
type="button"
|
|
96
209
|
className="absolute inset-0 bg-black/25"
|
|
97
210
|
onClick={() => setOpen(false)}
|
|
98
|
-
aria-label="Close AI
|
|
211
|
+
aria-label="Close Ask AI"
|
|
99
212
|
/>
|
|
100
|
-
<aside
|
|
213
|
+
<aside
|
|
214
|
+
className="absolute inset-y-0 right-0 flex w-full max-w-md flex-col border-l border-ph-border bg-ph-surface shadow-2xl"
|
|
215
|
+
data-test="suite-ask-ai-panel"
|
|
216
|
+
>
|
|
101
217
|
<div className="flex items-center justify-between border-b border-ph-border px-4 py-3">
|
|
102
|
-
<div className="
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
218
|
+
<div className="min-w-0">
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<Icon className="h-4 w-4 shrink-0 text-ph-brand" />
|
|
221
|
+
<span className="text-sm font-semibold text-ph-ink">
|
|
222
|
+
{title}
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
<p className="mt-0.5 text-[11px] text-ph-mutedtext">
|
|
226
|
+
Shared across your suite · same chat in every app
|
|
227
|
+
</p>
|
|
107
228
|
</div>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
229
|
+
<div className="flex items-center gap-1">
|
|
230
|
+
{messages.length > 0 ? (
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => void handleClear()}
|
|
234
|
+
disabled={loading}
|
|
235
|
+
className="rounded-md px-2 py-1 text-xs text-ph-mutedtext hover:bg-ph-muted hover:text-ph-ink disabled:opacity-50"
|
|
236
|
+
data-test="ask-ai-clear"
|
|
237
|
+
>
|
|
238
|
+
Clear
|
|
239
|
+
</button>
|
|
240
|
+
) : null}
|
|
120
241
|
<button
|
|
121
|
-
key={preset.label}
|
|
122
242
|
type="button"
|
|
123
|
-
onClick={() =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}}
|
|
127
|
-
disabled={loading}
|
|
128
|
-
className="rounded-md border border-ph-border px-2 py-1 text-xs text-ph-ink hover:bg-ph-muted disabled:opacity-50"
|
|
243
|
+
onClick={() => setOpen(false)}
|
|
244
|
+
className="rounded p-1 hover:bg-ph-muted"
|
|
245
|
+
aria-label="Close"
|
|
129
246
|
>
|
|
130
|
-
|
|
247
|
+
<X className="h-4 w-4" />
|
|
131
248
|
</button>
|
|
132
|
-
|
|
249
|
+
</div>
|
|
133
250
|
</div>
|
|
134
251
|
|
|
135
|
-
|
|
136
|
-
|
|
252
|
+
{presets.length > 0 && messages.length === 0 ? (
|
|
253
|
+
<div className="flex flex-wrap gap-1.5 border-b border-ph-border px-4 py-2">
|
|
254
|
+
{presets.map((preset) => (
|
|
255
|
+
<button
|
|
256
|
+
key={preset.label}
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={() => void ask(preset.prompt)}
|
|
259
|
+
disabled={loading || hydrating}
|
|
260
|
+
className="rounded-full border border-ph-border px-2.5 py-1 text-xs text-ph-ink hover:bg-ph-muted disabled:opacity-50"
|
|
261
|
+
>
|
|
262
|
+
{preset.label}
|
|
263
|
+
</button>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
) : null}
|
|
267
|
+
|
|
268
|
+
<div
|
|
269
|
+
ref={scrollerRef}
|
|
270
|
+
className="flex-1 space-y-3 overflow-y-auto p-4"
|
|
271
|
+
>
|
|
272
|
+
{error ? (
|
|
137
273
|
<p className="bg-ph-danger/10 rounded-lg px-3 py-2 text-sm text-ph-danger">
|
|
138
274
|
{error}
|
|
139
275
|
</p>
|
|
140
|
-
)}
|
|
141
|
-
|
|
276
|
+
) : null}
|
|
277
|
+
|
|
278
|
+
{hydrating && messages.length === 0 ? (
|
|
142
279
|
<div className="flex items-center gap-2 text-sm text-ph-mutedtext">
|
|
143
280
|
<Spinner className="h-4 w-4 animate-spin" />
|
|
144
|
-
|
|
281
|
+
Loading chat…
|
|
145
282
|
</div>
|
|
146
|
-
)}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
283
|
+
) : null}
|
|
284
|
+
|
|
285
|
+
{!hydrating && messages.length === 0 && !error ? (
|
|
286
|
+
<div className="mt-10 px-2 text-center">
|
|
287
|
+
<p className="font-display text-base font-semibold text-ph-ink">
|
|
288
|
+
How can I help?
|
|
289
|
+
</p>
|
|
290
|
+
<p className="mt-2 text-sm leading-relaxed text-ph-subtle">
|
|
291
|
+
{emptyState}
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
) : null}
|
|
295
|
+
|
|
296
|
+
{messages.map((message, index) => {
|
|
297
|
+
const isUser = message.role === 'user';
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
key={`${message.role}-${index}-${message.content.slice(0, 24)}`}
|
|
301
|
+
className={
|
|
302
|
+
isUser
|
|
303
|
+
? 'ml-8 rounded-2xl bg-ph-brand/10 px-3.5 py-2.5 text-sm text-ph-ink'
|
|
304
|
+
: 'mr-4 rounded-2xl bg-ph-muted px-3.5 py-2.5 text-sm text-ph-ink'
|
|
305
|
+
}
|
|
306
|
+
data-test={isUser ? 'ask-ai-user' : 'ask-ai-assistant'}
|
|
154
307
|
>
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
308
|
+
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
309
|
+
{!isUser ? (
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => void copyMessage(index, message.content)}
|
|
313
|
+
className="mt-2 inline-flex items-center gap-1 text-xs text-ph-mutedtext hover:text-ph-ink"
|
|
314
|
+
>
|
|
315
|
+
{copiedId === index ? (
|
|
316
|
+
<Check className="h-3 w-3" />
|
|
317
|
+
) : (
|
|
318
|
+
<Copy className="h-3 w-3" />
|
|
319
|
+
)}
|
|
320
|
+
{copiedId === index ? 'Copied' : 'Copy'}
|
|
321
|
+
</button>
|
|
322
|
+
) : null}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
326
|
+
|
|
327
|
+
{loading ? (
|
|
328
|
+
<div className="flex items-center gap-2 text-sm text-ph-mutedtext">
|
|
329
|
+
<Spinner className="h-4 w-4 animate-spin" />
|
|
330
|
+
Thinking…
|
|
162
331
|
</div>
|
|
163
|
-
)}
|
|
164
|
-
{!answer && !loading && !error && (
|
|
165
|
-
<p className="mt-8 text-center text-xs text-ph-mutedtext">
|
|
166
|
-
{emptyState}
|
|
167
|
-
</p>
|
|
168
|
-
)}
|
|
332
|
+
) : null}
|
|
169
333
|
</div>
|
|
170
334
|
|
|
171
335
|
<div className="border-t border-ph-border p-3">
|
|
@@ -182,13 +346,15 @@ export function SuiteAiPanel({
|
|
|
182
346
|
placeholder="Ask anything…"
|
|
183
347
|
rows={2}
|
|
184
348
|
disabled={loading}
|
|
185
|
-
|
|
349
|
+
data-test="ask-ai-input"
|
|
350
|
+
className="flex-1 resize-none rounded-xl border border-ph-border bg-ph-canvas p-2.5 text-sm text-ph-ink outline-none focus:ring-1 focus:ring-ph-brand"
|
|
186
351
|
/>
|
|
187
352
|
<button
|
|
188
353
|
type="button"
|
|
189
354
|
onClick={() => void ask(prompt)}
|
|
190
355
|
disabled={loading || !prompt.trim()}
|
|
191
|
-
|
|
356
|
+
data-test="ask-ai-send"
|
|
357
|
+
className="self-end rounded-xl bg-ph-brand px-3.5 py-2 text-sm font-medium text-white disabled:opacity-50"
|
|
192
358
|
>
|
|
193
359
|
Ask
|
|
194
360
|
</button>
|
|
@@ -196,7 +362,7 @@ export function SuiteAiPanel({
|
|
|
196
362
|
</div>
|
|
197
363
|
</aside>
|
|
198
364
|
</div>
|
|
199
|
-
)}
|
|
365
|
+
) : null}
|
|
200
366
|
</>
|
|
201
367
|
);
|
|
202
368
|
}
|
|
@@ -13,6 +13,11 @@ export interface SuiteAppLayoutProps {
|
|
|
13
13
|
sidebarWidth?: number;
|
|
14
14
|
/** True when the sidebar is collapsed to the icon rail. */
|
|
15
15
|
collapsed?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When true, `#main-content` does not scroll — the page owns an internal
|
|
18
|
+
* scrollport (e.g. Deep Research: fixed header + composer, chat scrolls).
|
|
19
|
+
*/
|
|
20
|
+
lockMainScroll?: boolean;
|
|
16
21
|
/** Drag this to resize the sidebar. */
|
|
17
22
|
onStartResize?: (e: PointerEvent<HTMLDivElement>) => void;
|
|
18
23
|
/** Double-click to reset sidebar width. */
|
|
@@ -21,10 +26,9 @@ export interface SuiteAppLayoutProps {
|
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
|
-
* Responsive ShellStack app shell.
|
|
25
|
-
*
|
|
26
|
-
* header +
|
|
27
|
-
* app should use.
|
|
29
|
+
* Responsive ShellStack app shell. One content tree (no duplicate children),
|
|
30
|
+
* one `#main-content` landmark. Desktop: fixed sidebar + scrollable main.
|
|
31
|
+
* Mobile: header + main + bottom nav.
|
|
28
32
|
*/
|
|
29
33
|
export function SuiteAppLayout({
|
|
30
34
|
sidebar,
|
|
@@ -33,36 +37,52 @@ export function SuiteAppLayout({
|
|
|
33
37
|
bottomNav,
|
|
34
38
|
sidebarWidth = 240,
|
|
35
39
|
collapsed = false,
|
|
40
|
+
lockMainScroll = false,
|
|
36
41
|
onStartResize,
|
|
37
42
|
onResetWidth,
|
|
38
43
|
className,
|
|
39
44
|
}: SuiteAppLayoutProps) {
|
|
40
45
|
return (
|
|
41
|
-
<div
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
<div
|
|
47
|
+
data-test="app-shell"
|
|
48
|
+
className={cn('flex min-h-dvh lg:h-screen lg:overflow-hidden', className)}
|
|
49
|
+
>
|
|
50
|
+
<aside
|
|
51
|
+
className="relative hidden shrink-0 flex-col overflow-hidden border-r border-ph-border transition-[width] duration-200 ease-out lg:flex"
|
|
52
|
+
style={{ width: `${sidebarWidth}px` }}
|
|
53
|
+
data-collapsed={collapsed ? 'true' : 'false'}
|
|
54
|
+
>
|
|
55
|
+
{sidebar}
|
|
56
|
+
{onStartResize ? (
|
|
57
|
+
<div
|
|
58
|
+
role="separator"
|
|
59
|
+
aria-orientation="vertical"
|
|
60
|
+
aria-label="Resize sidebar"
|
|
61
|
+
onPointerDown={onStartResize}
|
|
62
|
+
onDoubleClick={onResetWidth}
|
|
63
|
+
className="absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
|
|
64
|
+
/>
|
|
65
|
+
) : null}
|
|
66
|
+
</aside>
|
|
61
67
|
|
|
62
|
-
<div className="flex min-h-
|
|
63
|
-
{mobileHeader ?
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
69
|
+
{mobileHeader ? (
|
|
70
|
+
<div className="shrink-0 lg:hidden">{mobileHeader}</div>
|
|
71
|
+
) : null}
|
|
72
|
+
<main
|
|
73
|
+
id="main-content"
|
|
74
|
+
tabIndex={-1}
|
|
75
|
+
data-lock-scroll={lockMainScroll ? 'true' : undefined}
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none',
|
|
78
|
+
lockMainScroll ? 'overflow-hidden' : 'overflow-y-auto',
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</main>
|
|
83
|
+
{bottomNav ? (
|
|
84
|
+
<div className="shrink-0 lg:hidden">{bottomNav}</div>
|
|
85
|
+
) : null}
|
|
66
86
|
</div>
|
|
67
87
|
</div>
|
|
68
88
|
);
|