@xenide-io/the-old-ui-theme 0.6.14 → 0.7.1
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-VOCHIR3W.mjs → chunk-YB4TBWHV.mjs} +1423 -375
- package/dist/index.js +3 -1
- package/dist/index.mjs +43 -45
- package/dist/suite.d.mts +29 -3
- package/dist/suite.d.ts +29 -3
- package/dist/suite.js +1550 -325
- package/dist/suite.mjs +136 -44
- package/dist/ui.js +3 -1
- package/dist/ui.mjs +10 -12
- package/package.json +1 -1
- package/scripts/sync-posthog-icons.mjs +3 -4
- package/src/components/icons/icons.tsx +3 -1
- package/src/suite/components/app-switcher.test.tsx +96 -0
- package/src/suite/components/app-switcher.tsx +57 -39
- package/src/suite/components/suite-integration-picker.tsx +68 -0
- package/src/suite/components/suite-notification-bell.test.tsx +6 -5
- package/src/suite/components/suite-notification-bell.tsx +8 -6
- package/src/suite/index.ts +6 -0
- package/dist/chunk-E5ZBQYEX.mjs +0 -1073
package/dist/suite.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Button,
|
|
2
3
|
DropdownItem,
|
|
3
4
|
DropdownMenu,
|
|
5
|
+
Input,
|
|
6
|
+
Modal,
|
|
4
7
|
Tooltip
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YB4TBWHV.mjs";
|
|
6
9
|
import {
|
|
7
10
|
SuiteThemeProvider,
|
|
8
11
|
useSuiteTheme
|
|
@@ -896,7 +899,10 @@ function TodayEmptyState({
|
|
|
896
899
|
// src/suite/components/app-switcher.tsx
|
|
897
900
|
import Image from "next/image";
|
|
898
901
|
import { useState } from "react";
|
|
899
|
-
import {
|
|
902
|
+
import {
|
|
903
|
+
NavArrowDown as ChevronDown,
|
|
904
|
+
OpenNewWindow
|
|
905
|
+
} from "iconoir-react";
|
|
900
906
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
901
907
|
function appSwitcherTriggerClass(open, collapsed = false) {
|
|
902
908
|
return cn(
|
|
@@ -1013,43 +1019,53 @@ function AppSwitcher({
|
|
|
1013
1019
|
),
|
|
1014
1020
|
children: /* @__PURE__ */ jsxs6("div", { id: "app-switcher-menu", className: "p-1", "data-test": "app-switcher-menu", children: [
|
|
1015
1021
|
/* @__PURE__ */ jsx6("p", { className: "px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-ph-mutedtext", children: "Switch application" }),
|
|
1016
|
-
apps.map((app) => /* @__PURE__ */
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
/* @__PURE__ */
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
/* @__PURE__ */
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1022
|
+
apps.filter((app) => app.slug !== currentApp).map((app) => /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
|
|
1023
|
+
/* @__PURE__ */ jsx6(
|
|
1024
|
+
DropdownItem2,
|
|
1025
|
+
{
|
|
1026
|
+
id: `switch-app-${app.slug}`,
|
|
1027
|
+
"data-test": `switch-app-${app.slug}`,
|
|
1028
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1029
|
+
onClick: () => {
|
|
1030
|
+
setOpen(false);
|
|
1031
|
+
onSelect(app);
|
|
1032
|
+
},
|
|
1033
|
+
className: cn(appSwitcherMenuItemClass(), "pr-11"),
|
|
1034
|
+
children: /* @__PURE__ */ jsxs6("span", { className: "flex min-w-0 items-center gap-3", children: [
|
|
1035
|
+
/* @__PURE__ */ jsx6(
|
|
1036
|
+
Image,
|
|
1037
|
+
{
|
|
1038
|
+
src: app.icon,
|
|
1039
|
+
alt: "",
|
|
1040
|
+
width: 28,
|
|
1041
|
+
height: 28,
|
|
1042
|
+
className: "h-7 w-7 shrink-0 rounded-md object-contain"
|
|
1043
|
+
}
|
|
1044
|
+
),
|
|
1045
|
+
/* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx6("span", { className: "block truncate text-sm font-medium text-ph-ink", children: app.name }),
|
|
1047
|
+
/* @__PURE__ */ jsx6("span", { className: "block truncate text-xs text-ph-mutedtext", children: app.description })
|
|
1048
|
+
] })
|
|
1049
|
+
] })
|
|
1050
|
+
}
|
|
1051
|
+
),
|
|
1052
|
+
/* @__PURE__ */ jsx6(
|
|
1053
|
+
"button",
|
|
1054
|
+
{
|
|
1055
|
+
type: "button",
|
|
1056
|
+
id: `switch-app-${app.slug}-new-tab`,
|
|
1057
|
+
"data-test": `switch-app-${app.slug}-new-tab`,
|
|
1058
|
+
"aria-label": `Open ${app.name} in a new tab`,
|
|
1059
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1060
|
+
onClick: () => {
|
|
1061
|
+
setOpen(false);
|
|
1062
|
+
onSelect(app, { newTab: true });
|
|
1063
|
+
},
|
|
1064
|
+
className: "absolute right-1.5 top-1/2 z-[1] flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ph-brand/35",
|
|
1065
|
+
children: /* @__PURE__ */ jsx6(OpenNewWindow, { className: "h-4 w-4", "aria-hidden": "true" })
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
] }, app.slug))
|
|
1053
1069
|
] })
|
|
1054
1070
|
}
|
|
1055
1071
|
);
|
|
@@ -1286,10 +1302,10 @@ function SuiteNotificationBell({
|
|
|
1286
1302
|
panelClassName: "w-80 overflow-hidden p-0",
|
|
1287
1303
|
open,
|
|
1288
1304
|
onOpenChange,
|
|
1289
|
-
trigger: /* @__PURE__ */
|
|
1305
|
+
trigger: /* @__PURE__ */ jsx7("span", { className: "relative inline-flex h-11 w-11 items-center justify-center overflow-visible rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink", children: /* @__PURE__ */ jsxs7("span", { className: "relative inline-flex h-5 w-5", children: [
|
|
1290
1306
|
/* @__PURE__ */ jsx7(Bell, { className: "h-5 w-5", strokeWidth: 1.75, "aria-hidden": true }),
|
|
1291
|
-
unread > 0 ? /* @__PURE__ */ jsx7("span", { className: "pointer-events-none absolute right-
|
|
1292
|
-
] }),
|
|
1307
|
+
unread > 0 ? /* @__PURE__ */ jsx7("span", { className: "pointer-events-none absolute -right-2 -top-2 z-[1] flex h-4 min-w-4 items-center justify-center rounded-full bg-ph-brand px-1 text-[9px] font-semibold leading-none text-white ring-2 ring-ph-surface", children: unread > 9 ? "9+" : unread }) : null
|
|
1308
|
+
] }) }),
|
|
1293
1309
|
children: [
|
|
1294
1310
|
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-ph-border px-3 py-2", children: [
|
|
1295
1311
|
/* @__PURE__ */ jsx7("span", { className: "text-sm font-semibold text-ph-ink", children: "Notifications" }),
|
|
@@ -2727,7 +2743,7 @@ function SuiteAiPanel({
|
|
|
2727
2743
|
|
|
2728
2744
|
// src/suite/components/workspace-switcher.tsx
|
|
2729
2745
|
import { useState as useState8 } from "react";
|
|
2730
|
-
import { Building, Check
|
|
2746
|
+
import { Building, Check, NavArrowDown } from "iconoir-react";
|
|
2731
2747
|
import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2732
2748
|
function SuiteWorkspaceSwitcher({
|
|
2733
2749
|
workspaces = [],
|
|
@@ -2814,7 +2830,7 @@ function SuiteWorkspaceSwitcher({
|
|
|
2814
2830
|
workspace.description ? /* @__PURE__ */ jsx18("span", { className: "block truncate text-xs text-ph-mutedtext", children: workspace.description }) : null
|
|
2815
2831
|
] }),
|
|
2816
2832
|
workspace.id === currentId ? /* @__PURE__ */ jsx18(
|
|
2817
|
-
|
|
2833
|
+
Check,
|
|
2818
2834
|
{
|
|
2819
2835
|
className: "h-4 w-4 shrink-0 text-ph-brand",
|
|
2820
2836
|
"aria-hidden": "true"
|
|
@@ -3612,6 +3628,81 @@ function SuiteAppLayout({
|
|
|
3612
3628
|
}
|
|
3613
3629
|
);
|
|
3614
3630
|
}
|
|
3631
|
+
|
|
3632
|
+
// src/suite/components/suite-integration-picker.tsx
|
|
3633
|
+
import { useMemo, useState as useState10 } from "react";
|
|
3634
|
+
import { NavArrowDown as NavArrowDown2, NavArrowUp, Server as Plug, Xmark } from "iconoir-react";
|
|
3635
|
+
import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
3636
|
+
var DESCRIPTION_LIMIT = 90;
|
|
3637
|
+
function SuiteIntegrationPicker({
|
|
3638
|
+
open,
|
|
3639
|
+
items,
|
|
3640
|
+
connectedItems = [],
|
|
3641
|
+
loading = false,
|
|
3642
|
+
onClose,
|
|
3643
|
+
onConnect,
|
|
3644
|
+
onDisconnect,
|
|
3645
|
+
title = "Integrate an app",
|
|
3646
|
+
description = "Connect an app to this workspace securely.",
|
|
3647
|
+
dataTest = "suite-integration-picker"
|
|
3648
|
+
}) {
|
|
3649
|
+
const [query, setQuery] = useState10("");
|
|
3650
|
+
const [category, setCategory] = useState10("all");
|
|
3651
|
+
const [expanded, setExpanded] = useState10(null);
|
|
3652
|
+
const categories = useMemo(
|
|
3653
|
+
() => Array.from(new Set(items.flatMap((item) => {
|
|
3654
|
+
var _a;
|
|
3655
|
+
return (_a = item.categories) != null ? _a : [];
|
|
3656
|
+
}))).sort(),
|
|
3657
|
+
[items]
|
|
3658
|
+
);
|
|
3659
|
+
const filtered = items.filter((item) => {
|
|
3660
|
+
var _a;
|
|
3661
|
+
const haystack = `${item.label} ${item.description}`.toLowerCase();
|
|
3662
|
+
return haystack.includes(query.trim().toLowerCase()) && (category === "all" || ((_a = item.categories) != null ? _a : []).includes(category));
|
|
3663
|
+
});
|
|
3664
|
+
return /* @__PURE__ */ jsx24(Modal, { open, onClose, title, description, size: "xl", dataTest, children: /* @__PURE__ */ jsxs21("div", { className: "space-y-4", children: [
|
|
3665
|
+
/* @__PURE__ */ jsx24(Input, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search apps...", "aria-label": "Search apps" }),
|
|
3666
|
+
/* @__PURE__ */ jsx24("div", { className: "flex flex-wrap gap-2", role: "group", "aria-label": "Integration categories", children: ["all", ...categories].map((value) => /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setCategory(value), className: category === value ? "rounded-full bg-ph-brand px-3 py-1.5 text-xs font-semibold text-white" : "rounded-full border border-ph-border px-3 py-1.5 text-xs text-ph-subtle hover:bg-ph-muted", children: value === "all" ? "All apps" : value }, value)) }),
|
|
3667
|
+
loading ? /* @__PURE__ */ jsx24("p", { className: "text-sm text-ph-subtle", children: "Loading apps..." }) : /* @__PURE__ */ jsx24("div", { className: "grid max-h-[60vh] gap-3 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((item) => {
|
|
3668
|
+
var _a, _b;
|
|
3669
|
+
const isExpanded = expanded === item.id;
|
|
3670
|
+
const longDescription = item.description.length > DESCRIPTION_LIMIT;
|
|
3671
|
+
const text = isExpanded || !longDescription ? item.description : `${item.description.slice(0, DESCRIPTION_LIMIT).trimEnd()}...`;
|
|
3672
|
+
return /* @__PURE__ */ jsxs21("div", { className: "flex min-h-36 flex-col justify-between rounded-xl border border-ph-border bg-ph-surface p-4", children: [
|
|
3673
|
+
/* @__PURE__ */ jsx24("button", { type: "button", className: "text-left", onClick: () => longDescription && setExpanded(isExpanded ? null : item.id), "aria-expanded": isExpanded, children: /* @__PURE__ */ jsxs21("span", { className: "flex items-start gap-3", children: [
|
|
3674
|
+
/* @__PURE__ */ jsx24("span", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ph-muted text-ph-ink", children: (_a = item.icon) != null ? _a : /* @__PURE__ */ jsx24(Plug, { className: "h-5 w-5", "aria-hidden": true }) }),
|
|
3675
|
+
/* @__PURE__ */ jsxs21("span", { className: "min-w-0", children: [
|
|
3676
|
+
/* @__PURE__ */ jsx24("span", { className: "block font-medium text-ph-ink", children: item.label }),
|
|
3677
|
+
/* @__PURE__ */ jsx24("span", { className: "mt-1 block text-xs text-ph-subtle", children: text }),
|
|
3678
|
+
longDescription ? /* @__PURE__ */ jsxs21("span", { className: "mt-1 block text-xs font-medium text-ph-brand", children: [
|
|
3679
|
+
isExpanded ? "See less" : "See more",
|
|
3680
|
+
" ",
|
|
3681
|
+
isExpanded ? /* @__PURE__ */ jsx24(NavArrowUp, { className: "inline h-3 w-3" }) : /* @__PURE__ */ jsx24(NavArrowDown2, { className: "inline h-3 w-3" })
|
|
3682
|
+
] }) : null
|
|
3683
|
+
] })
|
|
3684
|
+
] }) }),
|
|
3685
|
+
/* @__PURE__ */ jsx24(Button, { type: "button", variant: "primary", className: "mt-3 self-start", disabled: Boolean(item.unavailableReason) || item.connecting, onClick: () => onConnect(item), children: /* @__PURE__ */ jsxs21("span", { className: "flex items-center gap-1.5", children: [
|
|
3686
|
+
/* @__PURE__ */ jsx24(Plug, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
3687
|
+
item.connecting ? "Connecting..." : (_b = item.unavailableReason) != null ? _b : "Connect"
|
|
3688
|
+
] }) })
|
|
3689
|
+
] }, item.id);
|
|
3690
|
+
}) }),
|
|
3691
|
+
connectedItems.length > 0 && onDisconnect ? /* @__PURE__ */ jsxs21("div", { className: "space-y-2 border-t border-ph-border pt-4", children: [
|
|
3692
|
+
/* @__PURE__ */ jsx24("h3", { className: "font-medium text-ph-ink", children: "Connected apps" }),
|
|
3693
|
+
connectedItems.map((item) => /* @__PURE__ */ jsxs21("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-ph-border bg-ph-muted p-3", children: [
|
|
3694
|
+
/* @__PURE__ */ jsxs21("span", { className: "flex min-w-0 items-center gap-2 text-sm text-ph-ink", children: [
|
|
3695
|
+
item.icon,
|
|
3696
|
+
/* @__PURE__ */ jsx24("span", { className: "truncate", children: item.label })
|
|
3697
|
+
] }),
|
|
3698
|
+
/* @__PURE__ */ jsx24(Button, { type: "button", variant: "danger", onClick: () => onDisconnect(item), children: /* @__PURE__ */ jsxs21("span", { className: "flex items-center gap-1.5", children: [
|
|
3699
|
+
/* @__PURE__ */ jsx24(Xmark, { className: "h-4 w-4", "aria-hidden": true }),
|
|
3700
|
+
"Disconnect"
|
|
3701
|
+
] }) })
|
|
3702
|
+
] }, item.id))
|
|
3703
|
+
] }) : null
|
|
3704
|
+
] }) });
|
|
3705
|
+
}
|
|
3615
3706
|
export {
|
|
3616
3707
|
APP_ACCENTS,
|
|
3617
3708
|
APP_GLYPHS,
|
|
@@ -3647,6 +3738,7 @@ export {
|
|
|
3647
3738
|
SuiteBreadcrumbs,
|
|
3648
3739
|
SuiteEmptyState,
|
|
3649
3740
|
SuiteIcon,
|
|
3741
|
+
SuiteIntegrationPicker,
|
|
3650
3742
|
SuiteMobileDrawer,
|
|
3651
3743
|
SuiteMobileHeader,
|
|
3652
3744
|
SuiteMotionProvider,
|
package/dist/ui.js
CHANGED
|
@@ -399,7 +399,9 @@ var IconPlayCircle = createIconBase({
|
|
|
399
399
|
)
|
|
400
400
|
});
|
|
401
401
|
var IconGridView = createIconBase({
|
|
402
|
-
paths: fill(
|
|
402
|
+
paths: fill(
|
|
403
|
+
"M3,3v8h8V3H3z M9,9H5V5h4V9z M3,13v8h8v-8H3z M9,19H5v-4h4V19z M13,3v8h8V3H13z M19,9h-4V5h4V9z M13,13v8h8v-8H13z M19,19h-4v-4h4V19z"
|
|
404
|
+
)
|
|
403
405
|
});
|
|
404
406
|
var IconListView = createIconBase({
|
|
405
407
|
paths: fill(
|
package/dist/ui.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AvatarGroup,
|
|
10
10
|
Badge,
|
|
11
11
|
Button,
|
|
12
|
+
ButtonChrome,
|
|
12
13
|
Calendar,
|
|
13
14
|
Caption,
|
|
14
15
|
Card,
|
|
@@ -20,6 +21,11 @@ import {
|
|
|
20
21
|
ComponentDocs,
|
|
21
22
|
Display,
|
|
22
23
|
Dot,
|
|
24
|
+
DropdownButton,
|
|
25
|
+
DropdownItem,
|
|
26
|
+
DropdownMenu,
|
|
27
|
+
DropdownRadioGroup,
|
|
28
|
+
DropdownRadioItem,
|
|
23
29
|
EmptyState,
|
|
24
30
|
FileUpload,
|
|
25
31
|
FilterBar,
|
|
@@ -32,6 +38,8 @@ import {
|
|
|
32
38
|
H3,
|
|
33
39
|
H4,
|
|
34
40
|
H5,
|
|
41
|
+
IconBase,
|
|
42
|
+
IconByName,
|
|
35
43
|
Input,
|
|
36
44
|
Kbd,
|
|
37
45
|
Label,
|
|
@@ -64,20 +72,10 @@ import {
|
|
|
64
72
|
Textarea,
|
|
65
73
|
ThemeDomSync,
|
|
66
74
|
ThemeSwitcher,
|
|
67
|
-
Toggle
|
|
68
|
-
} from "./chunk-VOCHIR3W.mjs";
|
|
69
|
-
import {
|
|
70
|
-
ButtonChrome,
|
|
71
|
-
DropdownButton,
|
|
72
|
-
DropdownItem,
|
|
73
|
-
DropdownMenu,
|
|
74
|
-
DropdownRadioGroup,
|
|
75
|
-
DropdownRadioItem,
|
|
76
|
-
IconBase,
|
|
77
|
-
IconByName,
|
|
75
|
+
Toggle,
|
|
78
76
|
Tooltip,
|
|
79
77
|
TooltipProvider
|
|
80
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-YB4TBWHV.mjs";
|
|
81
79
|
import "./chunk-FWCSY2DS.mjs";
|
|
82
80
|
export {
|
|
83
81
|
Accordion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design \u2014 optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@ const CACHE = resolve("scripts/.posthog-icons.tsx");
|
|
|
18
18
|
function extractPhBlock(source, name) {
|
|
19
19
|
const start = source.indexOf(`export function ${name}(`);
|
|
20
20
|
if (start < 0) return null;
|
|
21
|
-
const next = source.indexOf("\nexport function
|
|
21
|
+
const next = source.indexOf("\nexport function ", start + 10);
|
|
22
22
|
return source.slice(start, next > 0 ? next : start + 4000);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -34,9 +34,8 @@ function extractOurBlock(source, name) {
|
|
|
34
34
|
function normPaths(block) {
|
|
35
35
|
if (!block) return "";
|
|
36
36
|
const fills = [...block.matchAll(/fill\(\s*\n?\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
return [...fills, ...ds, ...attrs].join("|").replace(/\s/g, "");
|
|
37
|
+
const paths = [...block.matchAll(/\sd="([^"]+)"/g)].map((m) => m[1]);
|
|
38
|
+
return [...fills, ...paths].join("|").replace(/\s/g, "");
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
async function main() {
|
|
@@ -98,7 +98,9 @@ export const IconPlayCircle = createIconBase({
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
export const IconGridView = createIconBase({
|
|
101
|
-
paths: fill(
|
|
101
|
+
paths: fill(
|
|
102
|
+
"M3,3v8h8V3H3z M9,9H5V5h4V9z M3,13v8h8v-8H3z M9,19H5v-4h4V19z M13,3v8h8V3H13z M19,9h-4V5h4V9z M13,13v8h8v-8H13z M19,19h-4v-4h4V19z",
|
|
103
|
+
),
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
export const IconListView = createIconBase({
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
vi.mock("next/image", () => ({
|
|
7
|
+
default: (props: ComponentPropsWithoutRef<"img">) => <img {...props} />,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { AppSwitcher, type SuiteAppEntry } from "./app-switcher";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const APPS: SuiteAppEntry[] = [
|
|
17
|
+
{ slug: "tides", name: "Tides", description: "Projects", icon: "/tides.svg" },
|
|
18
|
+
{
|
|
19
|
+
slug: "turtletime",
|
|
20
|
+
name: "TurtleTime",
|
|
21
|
+
description: "Time",
|
|
22
|
+
icon: "/time.svg",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
slug: "kraken",
|
|
26
|
+
name: "Kraken",
|
|
27
|
+
description: "Documents",
|
|
28
|
+
icon: "/kraken.svg",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
slug: "nakama",
|
|
32
|
+
name: "Nakama",
|
|
33
|
+
description: "AI teammates",
|
|
34
|
+
icon: "/nakama.svg",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function TestDropdownMenu({
|
|
39
|
+
trigger,
|
|
40
|
+
children,
|
|
41
|
+
"aria-label": ariaLabel,
|
|
42
|
+
}: {
|
|
43
|
+
trigger: ReactNode;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
"aria-label": string;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<div aria-label={ariaLabel}>
|
|
49
|
+
{trigger}
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TestDropdownItem({
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: ComponentPropsWithoutRef<"button">) {
|
|
59
|
+
return <button {...props}>{children}</button>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("AppSwitcher", () => {
|
|
63
|
+
it("keeps the current app out of the simple application list", async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
const onSelect = vi.fn();
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<AppSwitcher
|
|
69
|
+
apps={APPS}
|
|
70
|
+
currentApp="tides"
|
|
71
|
+
onSelect={onSelect}
|
|
72
|
+
mark={<span />}
|
|
73
|
+
title={<span>Tides</span>}
|
|
74
|
+
dropdownMenu={TestDropdownMenu}
|
|
75
|
+
dropdownItem={TestDropdownItem}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const menu = document.querySelector("[data-test='app-switcher-menu']");
|
|
80
|
+
expect(menu).not.toHaveClass("grid-cols-2");
|
|
81
|
+
expect(menu).toHaveTextContent("Switch application");
|
|
82
|
+
expect(
|
|
83
|
+
document.querySelector("[data-test='switch-app-tides']"),
|
|
84
|
+
).not.toBeInTheDocument();
|
|
85
|
+
const newTabButton = screen.getByRole("button", {
|
|
86
|
+
name: "Open Kraken in a new tab",
|
|
87
|
+
});
|
|
88
|
+
expect(newTabButton).toHaveClass("absolute", "h-8", "w-8");
|
|
89
|
+
expect(
|
|
90
|
+
screen.queryByRole("button", { name: "Open Tides in a new tab" }),
|
|
91
|
+
).not.toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
await user.click(newTabButton);
|
|
94
|
+
expect(onSelect).toHaveBeenCalledWith(APPS[2], { newTab: true });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import Image from "next/image";
|
|
4
4
|
import { useState, type ReactNode } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
NavArrowDown as ChevronDown,
|
|
7
|
+
OpenNewWindow,
|
|
8
|
+
} from "iconoir-react";
|
|
6
9
|
|
|
7
10
|
import { cn } from "../lib/cn";
|
|
8
11
|
import type {
|
|
@@ -24,7 +27,6 @@ export function appSwitcherMarkClass() {
|
|
|
24
27
|
return "h-8 w-8 shrink-0 rounded-lg shadow-[0_1px_2px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.08)] ring-1 ring-black/[0.06]";
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
/** No sticky Radix highlight while waiting for cross-app redirect. */
|
|
28
30
|
export function appSwitcherMenuItemClass() {
|
|
29
31
|
return "rounded-lg px-2 py-2 data-[highlighted]:bg-transparent hover:bg-ph-muted/80";
|
|
30
32
|
}
|
|
@@ -101,6 +103,11 @@ export interface SuiteAppEntry {
|
|
|
101
103
|
icon: string;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
export interface SuiteAppSelectOptions {
|
|
107
|
+
/** Open the handoff in a separate browser tab when requested by a caller. */
|
|
108
|
+
newTab?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
/**
|
|
105
112
|
* Suite app switcher dropdown. Apps inject their brand mark/title, the
|
|
106
113
|
* visible app list, the cross-app navigation handler, and the theme
|
|
@@ -120,7 +127,7 @@ export function AppSwitcher({
|
|
|
120
127
|
}: {
|
|
121
128
|
apps: SuiteAppEntry[];
|
|
122
129
|
currentApp: string;
|
|
123
|
-
onSelect: (app: SuiteAppEntry) => void;
|
|
130
|
+
onSelect: (app: SuiteAppEntry, options?: SuiteAppSelectOptions) => void;
|
|
124
131
|
mark: ReactNode;
|
|
125
132
|
title: ReactNode;
|
|
126
133
|
collapsed?: boolean;
|
|
@@ -160,43 +167,54 @@ export function AppSwitcher({
|
|
|
160
167
|
<p className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-ph-mutedtext">
|
|
161
168
|
Switch application
|
|
162
169
|
</p>
|
|
163
|
-
{apps
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
170
|
+
{apps
|
|
171
|
+
.filter((app) => app.slug !== currentApp)
|
|
172
|
+
.map((app) => (
|
|
173
|
+
<div key={app.slug} className="relative">
|
|
174
|
+
<DropdownItem
|
|
175
|
+
id={`switch-app-${app.slug}`}
|
|
176
|
+
data-test={`switch-app-${app.slug}`}
|
|
177
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
178
|
+
onClick={() => {
|
|
179
|
+
setOpen(false);
|
|
180
|
+
onSelect(app);
|
|
181
|
+
}}
|
|
182
|
+
className={cn(appSwitcherMenuItemClass(), "pr-11")}
|
|
183
|
+
>
|
|
184
|
+
<span className="flex min-w-0 items-center gap-3">
|
|
185
|
+
<Image
|
|
186
|
+
src={app.icon}
|
|
187
|
+
alt=""
|
|
188
|
+
width={28}
|
|
189
|
+
height={28}
|
|
190
|
+
className="h-7 w-7 shrink-0 rounded-md object-contain"
|
|
191
|
+
/>
|
|
192
|
+
<span className="min-w-0 flex-1">
|
|
193
|
+
<span className="block truncate text-sm font-medium text-ph-ink">
|
|
194
|
+
{app.name}
|
|
195
|
+
</span>
|
|
196
|
+
<span className="block truncate text-xs text-ph-mutedtext">
|
|
197
|
+
{app.description}
|
|
198
|
+
</span>
|
|
199
|
+
</span>
|
|
189
200
|
</span>
|
|
190
|
-
</
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
</DropdownItem>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
id={`switch-app-${app.slug}-new-tab`}
|
|
205
|
+
data-test={`switch-app-${app.slug}-new-tab`}
|
|
206
|
+
aria-label={`Open ${app.name} in a new tab`}
|
|
207
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
208
|
+
onClick={() => {
|
|
209
|
+
setOpen(false);
|
|
210
|
+
onSelect(app, { newTab: true });
|
|
211
|
+
}}
|
|
212
|
+
className="absolute right-1.5 top-1/2 z-[1] flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ph-brand/35"
|
|
213
|
+
>
|
|
214
|
+
<OpenNewWindow className="h-4 w-4" aria-hidden="true" />
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
200
218
|
</div>
|
|
201
219
|
</DropdownMenu>
|
|
202
220
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
4
|
+
import { NavArrowDown, NavArrowUp, Server as Plug, Xmark } from "iconoir-react";
|
|
5
|
+
import { Button, Input, Modal } from "@/components/ui";
|
|
6
|
+
|
|
7
|
+
export interface SuiteIntegrationPickerItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
categories?: string[];
|
|
13
|
+
connecting?: boolean;
|
|
14
|
+
unavailableReason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SuiteIntegrationPickerProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
items: SuiteIntegrationPickerItem[];
|
|
20
|
+
connectedItems?: SuiteIntegrationPickerItem[];
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
onConnect: (item: SuiteIntegrationPickerItem) => void;
|
|
24
|
+
onDisconnect?: (item: SuiteIntegrationPickerItem) => void;
|
|
25
|
+
title?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
dataTest?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DESCRIPTION_LIMIT = 90;
|
|
31
|
+
|
|
32
|
+
export function SuiteIntegrationPicker({
|
|
33
|
+
open,
|
|
34
|
+
items,
|
|
35
|
+
connectedItems = [],
|
|
36
|
+
loading = false,
|
|
37
|
+
onClose,
|
|
38
|
+
onConnect,
|
|
39
|
+
onDisconnect,
|
|
40
|
+
title = "Integrate an app",
|
|
41
|
+
description = "Connect an app to this workspace securely.",
|
|
42
|
+
dataTest = "suite-integration-picker",
|
|
43
|
+
}: SuiteIntegrationPickerProps) {
|
|
44
|
+
const [query, setQuery] = useState("");
|
|
45
|
+
const [category, setCategory] = useState("all");
|
|
46
|
+
const [expanded, setExpanded] = useState<string | null>(null);
|
|
47
|
+
const categories = useMemo(
|
|
48
|
+
() => Array.from(new Set(items.flatMap((item) => item.categories ?? []))).sort(),
|
|
49
|
+
[items],
|
|
50
|
+
);
|
|
51
|
+
const filtered = items.filter((item) => {
|
|
52
|
+
const haystack = `${item.label} ${item.description}`.toLowerCase();
|
|
53
|
+
return haystack.includes(query.trim().toLowerCase()) && (category === "all" || (item.categories ?? []).includes(category));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Modal open={open} onClose={onClose} title={title} description={description} size="xl" dataTest={dataTest}>
|
|
58
|
+
<div className="space-y-4">
|
|
59
|
+
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search apps..." aria-label="Search apps" />
|
|
60
|
+
<div className="flex flex-wrap gap-2" role="group" aria-label="Integration categories">
|
|
61
|
+
{["all", ...categories].map((value) => <button key={value} type="button" onClick={() => setCategory(value)} className={category === value ? "rounded-full bg-ph-brand px-3 py-1.5 text-xs font-semibold text-white" : "rounded-full border border-ph-border px-3 py-1.5 text-xs text-ph-subtle hover:bg-ph-muted"}>{value === "all" ? "All apps" : value}</button>)}
|
|
62
|
+
</div>
|
|
63
|
+
{loading ? <p className="text-sm text-ph-subtle">Loading apps...</p> : <div className="grid max-h-[60vh] gap-3 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3">{filtered.map((item) => { const isExpanded = expanded === item.id; const longDescription = item.description.length > DESCRIPTION_LIMIT; const text = isExpanded || !longDescription ? item.description : `${item.description.slice(0, DESCRIPTION_LIMIT).trimEnd()}...`; return <div key={item.id} className="flex min-h-36 flex-col justify-between rounded-xl border border-ph-border bg-ph-surface p-4"><button type="button" className="text-left" onClick={() => longDescription && setExpanded(isExpanded ? null : item.id)} aria-expanded={isExpanded}><span className="flex items-start gap-3"><span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ph-muted text-ph-ink">{item.icon ?? <Plug className="h-5 w-5" aria-hidden />}</span><span className="min-w-0"><span className="block font-medium text-ph-ink">{item.label}</span><span className="mt-1 block text-xs text-ph-subtle">{text}</span>{longDescription ? <span className="mt-1 block text-xs font-medium text-ph-brand">{isExpanded ? "See less" : "See more"} {isExpanded ? <NavArrowUp className="inline h-3 w-3" /> : <NavArrowDown className="inline h-3 w-3" />}</span> : null}</span></span></button><Button type="button" variant="primary" className="mt-3 self-start" disabled={Boolean(item.unavailableReason) || item.connecting} onClick={() => onConnect(item)}><span className="flex items-center gap-1.5"><Plug className="h-3.5 w-3.5" aria-hidden />{item.connecting ? "Connecting..." : item.unavailableReason ?? "Connect"}</span></Button></div>; })}</div>}
|
|
64
|
+
{connectedItems.length > 0 && onDisconnect ? <div className="space-y-2 border-t border-ph-border pt-4"><h3 className="font-medium text-ph-ink">Connected apps</h3>{connectedItems.map((item) => <div key={item.id} className="flex items-center justify-between gap-3 rounded-lg border border-ph-border bg-ph-muted p-3"><span className="flex min-w-0 items-center gap-2 text-sm text-ph-ink">{item.icon}<span className="truncate">{item.label}</span></span><Button type="button" variant="danger" onClick={() => onDisconnect(item)}><span className="flex items-center gap-1.5"><Xmark className="h-4 w-4" aria-hidden />Disconnect</span></Button></div>)}</div> : null}
|
|
65
|
+
</div>
|
|
66
|
+
</Modal>
|
|
67
|
+
);
|
|
68
|
+
}
|