@xenide-io/the-old-ui-theme 0.6.7 → 0.6.8
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-3G43JEI3.mjs → chunk-VOCHIR3W.mjs} +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +64 -15
- package/dist/suite.d.ts +64 -15
- package/dist/suite.js +750 -758
- package/dist/suite.mjs +571 -585
- package/dist/ui.js +2 -2
- package/dist/ui.mjs +1 -1
- package/package.json +1 -1
- package/src/styles/suite-skin.css +6 -0
- package/src/styles/themes.css +35 -33
- package/src/suite/components/ai-panel.test.ts +14 -40
- package/src/suite/components/ai-panel.tsx +305 -633
- package/src/suite/components/suite-app-layout.tsx +3 -2
- package/src/suite/components/suite-layout.test.tsx +36 -2
- package/src/suite/components/suite-sidebar.tsx +235 -102
- package/src/suite/components/today-ui.tsx +1 -1
- package/src/suite/components/workspace-switcher.tsx +162 -0
- package/src/suite/index.ts +9 -0
- package/src/suite/lib/apps.test.ts +40 -0
- package/src/suite/lib/apps.ts +37 -0
- package/src/suite/lib/use-sidebar-width.ts +11 -1
|
@@ -58,7 +58,8 @@ export function SuiteAppLayout({
|
|
|
58
58
|
style={{ width: `${sidebarWidth}px` }}
|
|
59
59
|
data-collapsed={collapsed ? "true" : "false"}
|
|
60
60
|
>
|
|
61
|
-
{
|
|
61
|
+
{/* ponytail: isolate so Ask AI / BlockNote z-index cannot cover the handle */}
|
|
62
|
+
<div className="relative z-0 h-full min-h-0 isolate">{sidebar}</div>
|
|
62
63
|
{onStartResize ? (
|
|
63
64
|
<div
|
|
64
65
|
role="separator"
|
|
@@ -80,7 +81,7 @@ export function SuiteAppLayout({
|
|
|
80
81
|
onResizeBy?.(16);
|
|
81
82
|
}
|
|
82
83
|
}}
|
|
83
|
-
className="absolute inset-y-0
|
|
84
|
+
className="absolute inset-y-0 right-0 z-20 w-2 cursor-col-resize touch-none transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
|
|
84
85
|
/>
|
|
85
86
|
) : null}
|
|
86
87
|
</aside>
|
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
import { render, screen, within } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
|
+
import { sidebarColumnWidth } from "../lib/use-sidebar-width";
|
|
4
5
|
import { SuiteAppLayout } from "./suite-app-layout";
|
|
5
6
|
import { SuitePageHeader } from "./suite-layout";
|
|
6
7
|
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
describe("sidebarColumnWidth", () => {
|
|
11
|
+
it("uses the persisted nav width, including while Ask AI is open", () => {
|
|
12
|
+
expect(sidebarColumnWidth(320, false, false)).toBe(320);
|
|
13
|
+
expect(sidebarColumnWidth(320, false, true)).toBe(320);
|
|
14
|
+
expect(sidebarColumnWidth(56, true, true)).toBe(56);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("sidebar resize handle", () => {
|
|
19
|
+
it("stays above Ask AI contents so the column can still be dragged", () => {
|
|
20
|
+
render(
|
|
21
|
+
<SuiteAppLayout
|
|
22
|
+
sidebarWidth={320}
|
|
23
|
+
onStartResize={() => {}}
|
|
24
|
+
sidebar={
|
|
25
|
+
<div data-suite-chat-open="" className="relative z-[999] h-full">
|
|
26
|
+
Chat
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
Page
|
|
31
|
+
</SuiteAppLayout>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const handle = document.querySelector('[data-test="sidebar-resize-handle"]');
|
|
35
|
+
expect(handle).not.toBeNull();
|
|
36
|
+
expect(handle?.previousElementSibling).toHaveClass("isolate");
|
|
37
|
+
expect(handle).toHaveClass("z-20");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
7
41
|
describe("suite scroll ownership", () => {
|
|
8
42
|
it("keeps mobile chrome outside the main scrollport and locks only on request", () => {
|
|
9
43
|
render(
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { type ReactNode } from "react";
|
|
4
|
+
import { useEffect, type ReactNode } from "react";
|
|
5
|
+
import { NavArrowLeft, Sparks } from "iconoir-react";
|
|
5
6
|
|
|
6
|
-
import { cn } from "../lib/cn";
|
|
7
7
|
import { Tooltip } from "../../components/ui/Tooltip";
|
|
8
|
+
import { cn } from "../lib/cn";
|
|
9
|
+
import { SUITE_OPEN_ASK_AI_EVENT } from "./ai-panel";
|
|
8
10
|
import type { SuiteNavIcon } from "./suite-bottom-nav";
|
|
9
11
|
|
|
10
12
|
type CollapsedNode = ReactNode | ((collapsed: boolean) => ReactNode);
|
|
@@ -38,6 +40,13 @@ export interface SuiteSidebarProps {
|
|
|
38
40
|
className?: string;
|
|
39
41
|
/** Use the richer surface background instead of canvas. */
|
|
40
42
|
surface?: boolean;
|
|
43
|
+
/** When true, the nav body is replaced by `askAi`. */
|
|
44
|
+
askAiOpen?: boolean;
|
|
45
|
+
onAskAiOpenChange?: (open: boolean) => void;
|
|
46
|
+
/** Chat column shown while Ask AI mode is open. */
|
|
47
|
+
askAi?: ReactNode;
|
|
48
|
+
/** Expand a collapsed rail before opening Ask AI. */
|
|
49
|
+
onRequestExpand?: () => void;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
function renderNode(
|
|
@@ -49,12 +58,12 @@ function renderNode(
|
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
|
-
* Standardised ShellStack sidebar.
|
|
53
|
-
* so the chrome feels familiar across all apps:
|
|
61
|
+
* Standardised ShellStack sidebar.
|
|
54
62
|
* - app switcher + notification bell in a compact header
|
|
55
63
|
* - workspace switcher at the top of the nav body
|
|
56
|
-
* - primary nav items
|
|
57
|
-
* - avatar +
|
|
64
|
+
* - primary nav items, optional secondary tree
|
|
65
|
+
* - avatar + Ask AI in the footer (sign out lives on the avatar menu)
|
|
66
|
+
* - Ask AI mode replaces the whole column (back + thread + composer)
|
|
58
67
|
*/
|
|
59
68
|
export function SuiteSidebar({
|
|
60
69
|
appSwitcher,
|
|
@@ -67,122 +76,233 @@ export function SuiteSidebar({
|
|
|
67
76
|
collapsed = false,
|
|
68
77
|
className,
|
|
69
78
|
surface = false,
|
|
79
|
+
askAiOpen = false,
|
|
80
|
+
onAskAiOpenChange,
|
|
81
|
+
askAi,
|
|
82
|
+
onRequestExpand,
|
|
70
83
|
}: SuiteSidebarProps) {
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
const showAskAi = Boolean(onAskAiOpenChange && askAi);
|
|
85
|
+
const chatOpen = showAskAi && askAiOpen && !collapsed;
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!onAskAiOpenChange) return;
|
|
89
|
+
function onOpen() {
|
|
90
|
+
onRequestExpand?.();
|
|
91
|
+
onAskAiOpenChange?.(true);
|
|
92
|
+
}
|
|
93
|
+
window.addEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
|
|
94
|
+
return () => window.removeEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
|
|
95
|
+
}, [onAskAiOpenChange, onRequestExpand]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!chatOpen) return;
|
|
99
|
+
function onKey(event: KeyboardEvent) {
|
|
100
|
+
if (event.key === "Escape") onAskAiOpenChange?.(false);
|
|
101
|
+
}
|
|
102
|
+
window.addEventListener("keydown", onKey);
|
|
103
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
104
|
+
}, [chatOpen, onAskAiOpenChange]);
|
|
105
|
+
|
|
106
|
+
function toggleAskAi() {
|
|
107
|
+
if (askAiOpen) {
|
|
108
|
+
onAskAiOpenChange?.(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (collapsed) onRequestExpand?.();
|
|
112
|
+
onAskAiOpenChange?.(true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const askAiButton = (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
data-test="suite-ask-ai-toggle"
|
|
119
|
+
aria-pressed={askAiOpen}
|
|
120
|
+
aria-label={askAiOpen ? "Back to navigation" : "Shelly AI"}
|
|
121
|
+
onClick={toggleAskAi}
|
|
73
122
|
className={cn(
|
|
74
|
-
"flex h-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ph-focus-ring",
|
|
124
|
+
askAiOpen
|
|
125
|
+
? "bg-ph-brand/10 text-ph-brand"
|
|
126
|
+
: "text-ph-mutedtext hover:bg-ph-muted hover:text-ph-ink",
|
|
77
127
|
)}
|
|
78
128
|
>
|
|
79
|
-
|
|
129
|
+
<Sparks className="h-4 w-4" aria-hidden />
|
|
130
|
+
</button>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (chatOpen) {
|
|
134
|
+
return (
|
|
80
135
|
<div
|
|
136
|
+
data-suite-chat-open=""
|
|
81
137
|
className={cn(
|
|
82
|
-
"flex
|
|
138
|
+
"flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
83
139
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
84
|
-
|
|
85
|
-
? "flex-col items-center gap-1 px-1 py-2"
|
|
86
|
-
: "h-14 items-center justify-between gap-2 px-3",
|
|
140
|
+
className,
|
|
87
141
|
)}
|
|
88
142
|
>
|
|
89
|
-
<div
|
|
90
|
-
{
|
|
143
|
+
<div
|
|
144
|
+
className={cn(
|
|
145
|
+
"flex h-14 shrink-0 items-center gap-1 border-b border-ph-border px-2",
|
|
146
|
+
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<Tooltip content="Back to navigation">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
data-test="suite-ask-ai-back"
|
|
153
|
+
aria-label="Back to navigation"
|
|
154
|
+
onClick={() => onAskAiOpenChange?.(false)}
|
|
155
|
+
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-ph-subtle transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
156
|
+
>
|
|
157
|
+
<NavArrowLeft className="h-4 w-4" aria-hidden />
|
|
158
|
+
</button>
|
|
159
|
+
</Tooltip>
|
|
160
|
+
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-ph-ink">
|
|
161
|
+
Shelly AI
|
|
162
|
+
</span>
|
|
91
163
|
</div>
|
|
92
|
-
|
|
93
|
-
<div className="shrink-0">
|
|
94
|
-
{renderNode(notificationBell, collapsed)}
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
164
|
+
<div className="min-h-0 flex-1">{askAi}</div>
|
|
97
165
|
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className={cn(
|
|
172
|
+
"flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
173
|
+
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
174
|
+
className,
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
{appSwitcher || notificationBell ? (
|
|
178
|
+
<div
|
|
179
|
+
className={cn(
|
|
180
|
+
"flex shrink-0 border-b border-ph-border",
|
|
181
|
+
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
182
|
+
collapsed
|
|
183
|
+
? "flex-col items-center gap-1 px-1 py-2"
|
|
184
|
+
: "h-14 items-center justify-between gap-2 px-3",
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
<div className={cn("min-w-0", collapsed ? "shrink-0" : "flex-1")}>
|
|
188
|
+
{renderNode(appSwitcher, collapsed)}
|
|
189
|
+
</div>
|
|
190
|
+
{collapsed ? null : (
|
|
191
|
+
<div className="shrink-0">
|
|
192
|
+
{renderNode(notificationBell, collapsed)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
) : null}
|
|
98
197
|
|
|
99
|
-
{/* Nav body */}
|
|
100
198
|
<div
|
|
101
199
|
className={cn(
|
|
102
|
-
"flex min-h-0 flex-1 flex-col
|
|
200
|
+
"flex min-h-0 flex-1 flex-col",
|
|
103
201
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
202
|
+
chatOpen ? "min-h-0" : "overflow-y-auto",
|
|
104
203
|
collapsed ? "px-1.5 py-2" : "p-2",
|
|
105
204
|
)}
|
|
106
205
|
>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
</div>
|
|
206
|
+
<div className={cn("mb-3 w-full shrink-0", collapsed && "flex justify-center")}>
|
|
207
|
+
{renderNode(contextSwitcher, collapsed)}
|
|
208
|
+
</div>
|
|
111
209
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
collapsed
|
|
130
|
-
? "mx-auto size-11 shrink-0 justify-center gap-0 px-0 py-0"
|
|
131
|
-
: "w-full px-2.5 py-2",
|
|
132
|
-
)}
|
|
133
|
-
>
|
|
134
|
-
<span
|
|
135
|
-
className={cn(
|
|
136
|
-
"relative flex h-4 w-4 shrink-0 items-center justify-center",
|
|
137
|
-
item.iconClassName,
|
|
138
|
-
)}
|
|
139
|
-
aria-hidden
|
|
140
|
-
>
|
|
141
|
-
<Icon
|
|
142
|
-
className="h-full w-full"
|
|
143
|
-
strokeWidth={active ? 2 : 1.75}
|
|
144
|
-
/>
|
|
145
|
-
</span>
|
|
146
|
-
<span
|
|
147
|
-
className={cn(
|
|
148
|
-
"relative truncate",
|
|
149
|
-
active
|
|
150
|
-
? "text-ph-ink"
|
|
151
|
-
: "text-ph-subtle group-hover:text-ph-ink",
|
|
152
|
-
collapsed && "sr-only",
|
|
153
|
-
)}
|
|
154
|
-
>
|
|
155
|
-
{item.label}
|
|
156
|
-
</span>
|
|
157
|
-
{!collapsed && item.badge ? (
|
|
158
|
-
<span className="relative ml-auto shrink-0">
|
|
159
|
-
{item.badge}
|
|
160
|
-
</span>
|
|
161
|
-
) : null}
|
|
162
|
-
</Link>
|
|
163
|
-
);
|
|
164
|
-
return collapsed ? (
|
|
165
|
-
<Tooltip key={item.label} content={item.label} side="right">
|
|
166
|
-
{link}
|
|
167
|
-
</Tooltip>
|
|
168
|
-
) : (
|
|
169
|
-
link
|
|
170
|
-
);
|
|
171
|
-
})}
|
|
172
|
-
</div>
|
|
173
|
-
</nav>
|
|
174
|
-
|
|
175
|
-
{secondaryNav ? (
|
|
176
|
-
<div
|
|
177
|
-
className="min-h-0 flex-1"
|
|
178
|
-
data-test="suite-sidebar-secondary-nav"
|
|
179
|
-
>
|
|
180
|
-
{secondaryNav}
|
|
210
|
+
{chatOpen ? (
|
|
211
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
212
|
+
<div className="mb-2 flex shrink-0 items-center gap-1">
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
data-test="suite-ask-ai-back"
|
|
216
|
+
onClick={() => onAskAiOpenChange?.(false)}
|
|
217
|
+
className="inline-flex h-9 items-center gap-1 rounded-lg px-2 text-sm font-medium text-ph-subtle transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
218
|
+
>
|
|
219
|
+
<NavArrowLeft className="h-4 w-4" aria-hidden />
|
|
220
|
+
Nav
|
|
221
|
+
</button>
|
|
222
|
+
<span className="truncate text-sm font-semibold text-ph-ink">
|
|
223
|
+
Shelly AI
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="min-h-0 flex-1">{askAi}</div>
|
|
181
227
|
</div>
|
|
182
|
-
) :
|
|
228
|
+
) : (
|
|
229
|
+
<>
|
|
230
|
+
<nav aria-label="Pages" className="shrink-0 space-y-0.5">
|
|
231
|
+
<div className="space-y-0.5">
|
|
232
|
+
{navItems.map((item) => {
|
|
233
|
+
const Icon = item.icon;
|
|
234
|
+
const active = Boolean(item.active);
|
|
235
|
+
const link = (
|
|
236
|
+
<Link
|
|
237
|
+
key={item.label}
|
|
238
|
+
href={item.href}
|
|
239
|
+
id={item.id}
|
|
240
|
+
data-test={item.dataTest ?? "nav-link"}
|
|
241
|
+
aria-current={active ? "page" : undefined}
|
|
242
|
+
onClick={item.onClick}
|
|
243
|
+
className={cn(
|
|
244
|
+
"group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors",
|
|
245
|
+
active
|
|
246
|
+
? "bg-ph-muted"
|
|
247
|
+
: "text-ph-subtle hover:bg-ph-muted hover:text-ph-ink",
|
|
248
|
+
collapsed
|
|
249
|
+
? "mx-auto size-11 shrink-0 justify-center gap-0 px-0 py-0"
|
|
250
|
+
: "w-full px-2.5 py-2",
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
<span
|
|
254
|
+
className={cn(
|
|
255
|
+
"relative flex h-4 w-4 shrink-0 items-center justify-center",
|
|
256
|
+
item.iconClassName,
|
|
257
|
+
)}
|
|
258
|
+
aria-hidden
|
|
259
|
+
>
|
|
260
|
+
<Icon
|
|
261
|
+
className="h-full w-full"
|
|
262
|
+
strokeWidth={active ? 2 : 1.75}
|
|
263
|
+
/>
|
|
264
|
+
</span>
|
|
265
|
+
<span
|
|
266
|
+
className={cn(
|
|
267
|
+
"relative truncate",
|
|
268
|
+
active
|
|
269
|
+
? "text-ph-ink"
|
|
270
|
+
: "text-ph-subtle group-hover:text-ph-ink",
|
|
271
|
+
collapsed && "sr-only",
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
{item.label}
|
|
275
|
+
</span>
|
|
276
|
+
{!collapsed && item.badge ? (
|
|
277
|
+
<span className="relative ml-auto shrink-0">
|
|
278
|
+
{item.badge}
|
|
279
|
+
</span>
|
|
280
|
+
) : null}
|
|
281
|
+
</Link>
|
|
282
|
+
);
|
|
283
|
+
return collapsed ? (
|
|
284
|
+
<Tooltip key={item.label} content={item.label} side="right">
|
|
285
|
+
{link}
|
|
286
|
+
</Tooltip>
|
|
287
|
+
) : (
|
|
288
|
+
link
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
</div>
|
|
292
|
+
</nav>
|
|
293
|
+
|
|
294
|
+
{secondaryNav ? (
|
|
295
|
+
<div
|
|
296
|
+
className="min-h-0 flex-1"
|
|
297
|
+
data-test="suite-sidebar-secondary-nav"
|
|
298
|
+
>
|
|
299
|
+
{secondaryNav}
|
|
300
|
+
</div>
|
|
301
|
+
) : null}
|
|
302
|
+
</>
|
|
303
|
+
)}
|
|
183
304
|
</div>
|
|
184
305
|
|
|
185
|
-
{/* Footer */}
|
|
186
306
|
<div
|
|
187
307
|
className={cn(
|
|
188
308
|
"shrink-0 border-t border-ph-border",
|
|
@@ -194,16 +314,29 @@ export function SuiteSidebar({
|
|
|
194
314
|
className={cn(
|
|
195
315
|
"flex items-center gap-2",
|
|
196
316
|
collapsed
|
|
197
|
-
? "flex-col items-center justify-center [
|
|
317
|
+
? "flex-col items-center justify-center [&>div]:w-auto"
|
|
198
318
|
: "w-full min-w-0",
|
|
199
319
|
)}
|
|
200
320
|
>
|
|
201
321
|
{renderNode(userMenu, collapsed)}
|
|
202
|
-
{
|
|
322
|
+
{showAskAi ? (
|
|
323
|
+
<div
|
|
324
|
+
className={cn("shrink-0", collapsed ? "mt-2" : "ml-auto")}
|
|
325
|
+
>
|
|
326
|
+
<Tooltip content="Shelly AI" side={collapsed ? "right" : "top"}>
|
|
327
|
+
{askAiButton}
|
|
328
|
+
</Tooltip>
|
|
329
|
+
</div>
|
|
330
|
+
) : footerExtras ? (
|
|
203
331
|
<div className={cn("shrink-0", collapsed ? "mt-2" : "ml-auto")}>
|
|
204
332
|
{renderNode(footerExtras, collapsed)}
|
|
205
333
|
</div>
|
|
206
334
|
) : null}
|
|
335
|
+
{showAskAi && footerExtras ? (
|
|
336
|
+
<div className={cn("shrink-0", collapsed && "mt-2")}>
|
|
337
|
+
{renderNode(footerExtras, collapsed)}
|
|
338
|
+
</div>
|
|
339
|
+
) : null}
|
|
207
340
|
</div>
|
|
208
341
|
</div>
|
|
209
342
|
</div>
|
|
@@ -473,7 +473,7 @@ export function TodayLayout({
|
|
|
473
473
|
* Prefer onClick → openSuiteAskAi(); href is a fallback only.
|
|
474
474
|
*/
|
|
475
475
|
export function TodayAskAiCard({
|
|
476
|
-
title = '
|
|
476
|
+
title = 'Shelly AI',
|
|
477
477
|
description = 'Ask about your workspace, draft something, or look a fact up online.',
|
|
478
478
|
cta = 'Ask',
|
|
479
479
|
className,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { Building, Check, NavArrowDown } from "iconoir-react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import type {
|
|
8
|
+
SuiteDropdownItemComponent,
|
|
9
|
+
SuiteDropdownMenuComponent,
|
|
10
|
+
} from "../lib/injected";
|
|
11
|
+
import { appSwitcherMenuItemClass } from "./app-switcher";
|
|
12
|
+
|
|
13
|
+
export interface SuiteWorkspaceEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SuiteWorkspaceGroup {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
workspaces: SuiteWorkspaceEntry[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Suite workspace switcher. Full-width bordered trigger; dropdown menu.
|
|
27
|
+
* Apps inject workspace rows (or labelled groups) and the theme dropdown.
|
|
28
|
+
*/
|
|
29
|
+
export function SuiteWorkspaceSwitcher({
|
|
30
|
+
workspaces = [],
|
|
31
|
+
groups,
|
|
32
|
+
currentId,
|
|
33
|
+
onSelect,
|
|
34
|
+
name,
|
|
35
|
+
collapsed = false,
|
|
36
|
+
emptyLabel = "No workspaces yet.",
|
|
37
|
+
dropdownMenu: DropdownMenu,
|
|
38
|
+
dropdownItem: DropdownItem,
|
|
39
|
+
triggerId,
|
|
40
|
+
triggerDataTest,
|
|
41
|
+
footer,
|
|
42
|
+
}: {
|
|
43
|
+
workspaces?: SuiteWorkspaceEntry[];
|
|
44
|
+
groups?: SuiteWorkspaceGroup[];
|
|
45
|
+
currentId?: string | null;
|
|
46
|
+
onSelect: (workspace: SuiteWorkspaceEntry) => void;
|
|
47
|
+
name: string;
|
|
48
|
+
collapsed?: boolean;
|
|
49
|
+
emptyLabel?: string;
|
|
50
|
+
dropdownMenu: SuiteDropdownMenuComponent;
|
|
51
|
+
dropdownItem: SuiteDropdownItemComponent;
|
|
52
|
+
triggerId?: string;
|
|
53
|
+
triggerDataTest?: string;
|
|
54
|
+
footer?: ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const sections =
|
|
58
|
+
groups?.filter((group) => group.workspaces.length > 0) ??
|
|
59
|
+
(workspaces.length > 0
|
|
60
|
+
? [{ id: "workspaces", label: "Switch workspace", workspaces }]
|
|
61
|
+
: []);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<DropdownMenu
|
|
65
|
+
aria-label="Switch workspace"
|
|
66
|
+
triggerId={triggerId ?? "workspace-switcher-trigger"}
|
|
67
|
+
triggerDataTest={triggerDataTest ?? "workspace-switcher-trigger"}
|
|
68
|
+
className={
|
|
69
|
+
collapsed
|
|
70
|
+
? "w-11 shrink-0"
|
|
71
|
+
: "w-full min-w-0 max-w-full [&_.ph-dropdown-trigger]:w-full"
|
|
72
|
+
}
|
|
73
|
+
panelClassName="w-64 max-w-full"
|
|
74
|
+
align="start"
|
|
75
|
+
open={open}
|
|
76
|
+
onOpenChange={setOpen}
|
|
77
|
+
trigger={
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex min-w-0 max-w-full items-center gap-2 rounded-lg border border-ph-border bg-ph-muted text-left transition-colors hover:bg-ph-muted",
|
|
81
|
+
collapsed ? "h-10 w-10 justify-center p-0" : "w-full px-3 py-2",
|
|
82
|
+
open && "border-ph-brand/40",
|
|
83
|
+
)}
|
|
84
|
+
data-test="workspace-switcher-trigger"
|
|
85
|
+
aria-expanded={open}
|
|
86
|
+
>
|
|
87
|
+
<Building className="h-4 w-4 shrink-0 text-ph-ink" aria-hidden />
|
|
88
|
+
{!collapsed ? (
|
|
89
|
+
<>
|
|
90
|
+
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-ph-ink">
|
|
91
|
+
{name}
|
|
92
|
+
</span>
|
|
93
|
+
<NavArrowDown
|
|
94
|
+
className={cn(
|
|
95
|
+
"h-3.5 w-3.5 shrink-0 text-ph-mutedtext transition-transform",
|
|
96
|
+
open && "rotate-180",
|
|
97
|
+
)}
|
|
98
|
+
aria-hidden
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
) : null}
|
|
102
|
+
</span>
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
<div
|
|
106
|
+
id="workspace-switcher-menu"
|
|
107
|
+
className="p-1"
|
|
108
|
+
data-test="workspace-switcher-menu"
|
|
109
|
+
>
|
|
110
|
+
{sections.length === 0 ? (
|
|
111
|
+
<p className="px-3 py-2 text-sm text-ph-mutedtext">{emptyLabel}</p>
|
|
112
|
+
) : (
|
|
113
|
+
sections.map((section) => (
|
|
114
|
+
<div key={section.id}>
|
|
115
|
+
<p className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-ph-mutedtext">
|
|
116
|
+
{section.label}
|
|
117
|
+
</p>
|
|
118
|
+
{section.workspaces.map((workspace) => (
|
|
119
|
+
<DropdownItem
|
|
120
|
+
key={workspace.id}
|
|
121
|
+
id={`workspace-option-${workspace.id}`}
|
|
122
|
+
role="option"
|
|
123
|
+
aria-selected={workspace.id === currentId}
|
|
124
|
+
data-test="workspace-option"
|
|
125
|
+
data-workspace-id={workspace.id}
|
|
126
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
127
|
+
onClick={() => {
|
|
128
|
+
setOpen(false);
|
|
129
|
+
onSelect(workspace);
|
|
130
|
+
}}
|
|
131
|
+
className={appSwitcherMenuItemClass()}
|
|
132
|
+
>
|
|
133
|
+
<span className="flex min-w-0 w-full items-center gap-3">
|
|
134
|
+
<span className="min-w-0 flex-1">
|
|
135
|
+
<span className="block truncate text-sm font-medium text-ph-ink">
|
|
136
|
+
{workspace.name}
|
|
137
|
+
</span>
|
|
138
|
+
{workspace.description ? (
|
|
139
|
+
<span className="block truncate text-xs text-ph-mutedtext">
|
|
140
|
+
{workspace.description}
|
|
141
|
+
</span>
|
|
142
|
+
) : null}
|
|
143
|
+
</span>
|
|
144
|
+
{workspace.id === currentId ? (
|
|
145
|
+
<Check
|
|
146
|
+
className="h-4 w-4 shrink-0 text-ph-brand"
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
/>
|
|
149
|
+
) : null}
|
|
150
|
+
</span>
|
|
151
|
+
</DropdownItem>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
))
|
|
155
|
+
)}
|
|
156
|
+
{footer ? (
|
|
157
|
+
<div className="mt-1 border-t border-ph-border pt-1">{footer}</div>
|
|
158
|
+
) : null}
|
|
159
|
+
</div>
|
|
160
|
+
</DropdownMenu>
|
|
161
|
+
);
|
|
162
|
+
}
|
package/src/suite/index.ts
CHANGED
|
@@ -35,8 +35,10 @@ export {
|
|
|
35
35
|
SUITE_APP_MAP,
|
|
36
36
|
suiteAppBaseUrl,
|
|
37
37
|
resolveSuiteNotificationHref,
|
|
38
|
+
classifySuiteHref,
|
|
38
39
|
type SuiteAppDefinition,
|
|
39
40
|
type SuiteAppSlug,
|
|
41
|
+
type SuiteHrefTarget,
|
|
40
42
|
} from "./lib/apps";
|
|
41
43
|
export { TodayCalibrating } from "./components/today-calibrating";
|
|
42
44
|
export {
|
|
@@ -50,11 +52,17 @@ export {
|
|
|
50
52
|
export {
|
|
51
53
|
SuiteAiPanel,
|
|
52
54
|
openSuiteAskAi,
|
|
55
|
+
consumePendingAskAiPrompt,
|
|
53
56
|
SUITE_OPEN_ASK_AI_EVENT,
|
|
54
57
|
type SuiteAiPreset,
|
|
55
58
|
type SuiteAiChatMessage,
|
|
56
59
|
type SuiteAskAiOpenDetail,
|
|
57
60
|
} from "./components/ai-panel";
|
|
61
|
+
export {
|
|
62
|
+
SuiteWorkspaceSwitcher,
|
|
63
|
+
type SuiteWorkspaceEntry,
|
|
64
|
+
type SuiteWorkspaceGroup,
|
|
65
|
+
} from "./components/workspace-switcher";
|
|
58
66
|
export {
|
|
59
67
|
APP_ACCENTS,
|
|
60
68
|
APP_GLYPHS,
|
|
@@ -85,6 +93,7 @@ export {
|
|
|
85
93
|
SIDEBAR_MAX_WIDTH,
|
|
86
94
|
SIDEBAR_MIN_EXPANDED_WIDTH,
|
|
87
95
|
SIDEBAR_RAIL_WIDTH,
|
|
96
|
+
sidebarColumnWidth,
|
|
88
97
|
useSidebarWidth,
|
|
89
98
|
} from "./lib/use-sidebar-width";
|
|
90
99
|
export { cn } from "./lib/cn";
|