@xenide-io/the-old-ui-theme 0.6.6 → 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/{ai-message-blocknote-4Q4U2T2W.mjs → ai-message-blocknote-DIUICX3G.mjs} +1 -1
- package/dist/{chunk-IAH7Z46I.mjs → chunk-VIWW7AW2.mjs} +3 -0
- 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 +753 -758
- package/dist/suite.mjs +573 -587
- package/dist/ui.js +2 -2
- package/dist/ui.mjs +1 -1
- package/package.json +1 -1
- package/src/app/globals.css +8 -1
- package/src/styles/suite-skin.css +6 -0
- package/src/styles/themes.css +36 -34
- 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/theme-provider.test.tsx +48 -0
- package/src/suite/components/theme-provider.tsx +4 -0
- 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
|
@@ -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";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { classifySuiteHref } from "./apps";
|
|
4
|
+
|
|
5
|
+
const BASES = {
|
|
6
|
+
tides: "http://localhost:3001",
|
|
7
|
+
turtletime: "http://localhost:3000",
|
|
8
|
+
kraken: "http://localhost:3003",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("classifySuiteHref", () => {
|
|
12
|
+
it("maps a Tides task URL to the Tides app and path", () => {
|
|
13
|
+
const match = classifySuiteHref(
|
|
14
|
+
"http://localhost:3001/dashboard/projects/abc/tasks/def",
|
|
15
|
+
BASES,
|
|
16
|
+
"http://localhost:3000",
|
|
17
|
+
);
|
|
18
|
+
expect(match).toEqual({
|
|
19
|
+
slug: "tides",
|
|
20
|
+
path: "/dashboard/projects/abc/tasks/def",
|
|
21
|
+
sameApp: false,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("marks a same-origin TurtleTime link as sameApp", () => {
|
|
26
|
+
const match = classifySuiteHref(
|
|
27
|
+
"http://localhost:3000/tracker",
|
|
28
|
+
BASES,
|
|
29
|
+
"http://localhost:3000",
|
|
30
|
+
);
|
|
31
|
+
expect(match?.sameApp).toBe(true);
|
|
32
|
+
expect(match?.path).toBe("/tracker");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ignores urls that are not a suite app", () => {
|
|
36
|
+
expect(
|
|
37
|
+
classifySuiteHref("https://example.com/x", BASES, "http://localhost:3000"),
|
|
38
|
+
).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/suite/lib/apps.ts
CHANGED
|
@@ -114,3 +114,40 @@ export function resolveSuiteNotificationHref(
|
|
|
114
114
|
if (!slug) return path;
|
|
115
115
|
return `${suiteAppBaseUrl(slug)}${path}`;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
export interface SuiteHrefTarget {
|
|
119
|
+
slug: SuiteAppSlug;
|
|
120
|
+
path: string;
|
|
121
|
+
sameApp: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Match a URL to a suite app origin so Ask AI links can switch apps. */
|
|
125
|
+
export function classifySuiteHref(
|
|
126
|
+
href: string,
|
|
127
|
+
bases: Partial<Record<SuiteAppSlug, string>>,
|
|
128
|
+
currentOrigin = '',
|
|
129
|
+
): SuiteHrefTarget | null {
|
|
130
|
+
let url: URL;
|
|
131
|
+
try {
|
|
132
|
+
url = new URL(href, currentOrigin || 'http://local.invalid');
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
137
|
+
for (const slug of Object.keys(bases) as SuiteAppSlug[]) {
|
|
138
|
+
const base = bases[slug];
|
|
139
|
+
if (!base) continue;
|
|
140
|
+
try {
|
|
141
|
+
if (new URL(base).origin !== url.origin) continue;
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const path = `${url.pathname}${url.search}${url.hash}` || '/';
|
|
146
|
+
return {
|
|
147
|
+
slug,
|
|
148
|
+
path,
|
|
149
|
+
sameApp: Boolean(currentOrigin && url.origin === currentOrigin),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
@@ -10,9 +10,19 @@ import {
|
|
|
10
10
|
export const SIDEBAR_RAIL_WIDTH = 56;
|
|
11
11
|
export const SIDEBAR_COLLAPSE_THRESHOLD = 80;
|
|
12
12
|
export const SIDEBAR_MIN_EXPANDED_WIDTH = 180;
|
|
13
|
-
export const SIDEBAR_MAX_WIDTH =
|
|
13
|
+
export const SIDEBAR_MAX_WIDTH = 640;
|
|
14
14
|
export const SIDEBAR_DEFAULT_WIDTH = 320;
|
|
15
15
|
|
|
16
|
+
/** Desktop column width: icon rail when collapsed, otherwise the persisted width. */
|
|
17
|
+
export function sidebarColumnWidth(
|
|
18
|
+
width: number,
|
|
19
|
+
collapsed: boolean,
|
|
20
|
+
_chatOpen = false,
|
|
21
|
+
): number {
|
|
22
|
+
if (collapsed) return SIDEBAR_RAIL_WIDTH;
|
|
23
|
+
return width;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
function clampWidth(width: number): number {
|
|
17
27
|
return Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_RAIL_WIDTH, width));
|
|
18
28
|
}
|