@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.
@@ -58,7 +58,8 @@ export function SuiteAppLayout({
58
58
  style={{ width: `${sidebarWidth}px` }}
59
59
  data-collapsed={collapsed ? "true" : "false"}
60
60
  >
61
- {sidebar}
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 -right-1 z-10 w-2 cursor-col-resize touch-none transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
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. Matches the original TurtleTime layout
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 styled like the original NavLink
57
- * - avatar + optional logout in the footer
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
- return (
72
- <div
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-full w-full min-w-0 flex-col text-ph-ink",
75
- surface ? "bg-ph-surface" : "bg-ph-canvas",
76
- className,
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
- {/* Header */}
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 shrink-0 border-b border-ph-border",
138
+ "flex h-full w-full min-w-0 flex-col text-ph-ink",
83
139
  surface ? "bg-ph-surface" : "bg-ph-canvas",
84
- collapsed
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 className={cn("min-w-0", collapsed ? "shrink-0" : "flex-1")}>
90
- {renderNode(appSwitcher, collapsed)}
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
- {collapsed ? null : (
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 overflow-y-auto",
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
- <nav aria-label="Pages" className="shrink-0 space-y-0.5">
108
- <div className={cn("mb-3", collapsed && "flex justify-center")}>
109
- {renderNode(contextSwitcher, collapsed)}
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
- <div className="space-y-0.5">
113
- {navItems.map((item) => {
114
- const Icon = item.icon;
115
- const active = Boolean(item.active);
116
- const link = (
117
- <Link
118
- key={item.label}
119
- href={item.href}
120
- id={item.id}
121
- data-test={item.dataTest ?? "nav-link"}
122
- aria-current={active ? "page" : undefined}
123
- onClick={item.onClick}
124
- className={cn(
125
- "group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors",
126
- active
127
- ? "bg-ph-muted"
128
- : "text-ph-subtle hover:bg-ph-muted hover:text-ph-ink",
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
- ) : null}
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 [&_[data-test$='-visible-sign-out']]:hidden [&>div]:w-auto"
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
- {footerExtras ? (
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 = 'Ask AI',
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
+ }
@@ -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";