@xenide-io/the-old-ui-theme 0.7.0 → 0.9.0
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/README.md +9 -10
- package/dist/{Chip-Cq3AiAji.d.mts → Chip-BzuNgJIa.d.mts} +81 -23
- package/dist/{Chip-Cq3AiAji.d.ts → Chip-BzuNgJIa.d.ts} +81 -23
- package/dist/{ai-message-blocknote-WABTHFY7.mjs → ai-message-blocknote-D4Z6DUCJ.mjs} +9 -3
- package/dist/chunk-2EO5VCXP.mjs +3912 -0
- package/dist/chunk-APKRZQRO.mjs +251 -0
- package/dist/chunk-BS7BZI5W.mjs +19 -0
- package/dist/chunk-IULORI3A.mjs +8 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +997 -576
- package/dist/index.mjs +49 -45
- package/dist/suite.d.mts +157 -55
- package/dist/suite.d.ts +157 -55
- package/dist/suite.js +4249 -801
- package/dist/suite.mjs +2578 -594
- package/dist/theme-boot-P-oqKVZF.d.mts +74 -0
- package/dist/theme-boot-P-oqKVZF.d.ts +74 -0
- package/dist/theme-boot.d.mts +2 -0
- package/dist/theme-boot.d.ts +2 -0
- package/dist/theme-boot.js +44 -0
- package/dist/theme-boot.mjs +8 -0
- package/dist/ui.d.mts +5 -29
- package/dist/ui.d.ts +5 -29
- package/dist/ui.js +647 -352
- package/dist/ui.mjs +10 -12
- package/docs/README.md +34 -0
- package/docs/ai-quick-reference.md +56 -0
- package/docs/component-library-improvement-plan.md +751 -0
- package/docs/component-reference.md +81 -0
- package/docs/components.md +48 -0
- package/docs/demos.md +19 -0
- package/docs/installation.md +117 -0
- package/docs/suite-audit.md +34 -0
- package/docs/theme-token-reference.md +67 -0
- package/docs/themes.md +164 -0
- package/package.json +22 -21
- package/public/fonts/OpenRunde-Bold.woff2 +0 -0
- package/public/fonts/OpenRunde-Medium.woff2 +0 -0
- package/public/fonts/OpenRunde-Regular.woff2 +0 -0
- package/public/fonts/OpenRunde-Semibold.woff2 +0 -0
- package/scripts/package-smoke.mjs +43 -0
- package/src/app/globals.css +2 -1
- package/src/components/ui/Accordion.tsx +20 -6
- package/src/components/ui/Avatar.tsx +5 -3
- package/src/components/ui/Calendar.tsx +187 -63
- package/src/components/ui/Card.test.tsx +38 -0
- package/src/components/ui/Card.tsx +77 -14
- package/src/components/ui/CollapsibleSection.tsx +12 -4
- package/src/components/ui/CommandPalette.test.tsx +91 -4
- package/src/components/ui/CommandPalette.tsx +138 -128
- package/src/components/ui/EmptyState.tsx +2 -2
- package/src/components/ui/FormField.tsx +1 -1
- package/src/components/ui/Input.tsx +42 -18
- package/src/components/ui/Kbd.tsx +18 -3
- package/src/components/ui/Modal.tsx +3 -1
- package/src/components/ui/Progress.tsx +14 -5
- package/src/components/ui/SegmentedControl.tsx +2 -2
- package/src/components/ui/SettingsLayout.tsx +1 -1
- package/src/components/ui/Stat.tsx +3 -2
- package/src/components/ui/Table.tsx +8 -3
- package/src/components/ui/Tooltip.test.tsx +70 -1
- package/src/components/ui/Tooltip.tsx +53 -29
- package/src/components/ui/Typography.tsx +103 -49
- package/src/components/ui/avatar-stat.test.tsx +23 -0
- package/src/components/ui/calendar.test.tsx +88 -0
- package/src/components/ui/disclosure.test.tsx +39 -0
- package/src/components/ui/index.ts +44 -6
- package/src/components/ui/progress.test.tsx +15 -0
- package/src/components/ui/typography.test.tsx +26 -1
- package/src/styles/suite-skin.css +8 -2
- package/src/styles/themes.css +184 -68
- package/src/suite/components/ai-message-blocknote.tsx +13 -2
- package/src/suite/components/ai-panel.tsx +41 -10
- package/src/suite/components/app-switcher.test.tsx +21 -6
- package/src/suite/components/app-switcher.tsx +7 -11
- package/src/suite/components/suite-app-layout.tsx +12 -3
- package/src/suite/components/suite-bottom-nav.tsx +0 -1
- package/src/suite/components/suite-clients-projects-directory.test.tsx +71 -0
- package/src/suite/components/suite-clients-projects-directory.tsx +943 -0
- package/src/suite/components/suite-integration-picker.test.tsx +224 -0
- package/src/suite/components/suite-integration-picker.tsx +939 -0
- package/src/suite/components/suite-integration-rules.test.tsx +157 -0
- package/src/suite/components/suite-integration-rules.tsx +471 -0
- package/src/suite/components/suite-mobile-drawer.test.tsx +18 -2
- package/src/suite/components/suite-mobile-drawer.tsx +65 -98
- package/src/suite/components/suite-notification-bell.tsx +29 -4
- package/src/suite/components/suite-sidebar.tsx +25 -25
- package/src/suite/components/suite-skeleton.tsx +2 -2
- package/src/suite/components/suite-user-menu.tsx +9 -8
- package/src/suite/components/theme-provider.test.tsx +87 -3
- package/src/suite/components/theme-provider.tsx +144 -23
- package/src/suite/components/today-ui.tsx +79 -68
- package/src/suite/components/workspace-switcher.test.tsx +56 -0
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/icons/app-accents.ts +12 -12
- package/src/suite/icons/glyphs.ts +6 -12
- package/src/suite/index.ts +27 -0
- package/src/suite/lib/apps.test.ts +9 -1
- package/src/suite/lib/apps.ts +14 -8
- package/src/suite/lib/cookies.ts +53 -0
- package/src/suite/lib/theme-boot.ts +32 -0
- package/src/suite/lib/theme-cookie.ts +7 -0
- package/src/suite/lib/use-sidebar-width.ts +5 -51
- package/dist/chunk-KDR6HI6F.mjs +0 -1075
- package/dist/chunk-NOI42JNZ.mjs +0 -151
- package/dist/chunk-VXISSXTB.mjs +0 -2564
- package/src/suite/lib/use-lock-mobile-page-zoom.test.tsx +0 -65
- package/src/suite/lib/use-lock-mobile-page-zoom.ts +0 -51
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
4
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
5
|
import { Xmark as X } from "iconoir-react";
|
|
5
6
|
|
|
6
7
|
import { cn } from "../lib/cn";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Suite mobile navigation drawer. Backdrop fade + panel slide, Escape to
|
|
10
|
-
* close, body scroll-lock and focus
|
|
11
|
-
* app-side via `children
|
|
11
|
+
* close, body scroll-lock and focus containment while open. Nav content stays
|
|
12
|
+
* app-side via `children`; Radix owns the modal interaction model.
|
|
12
13
|
*/
|
|
13
14
|
export function SuiteMobileDrawer({
|
|
14
15
|
open,
|
|
@@ -57,13 +58,6 @@ export function SuiteMobileDrawer({
|
|
|
57
58
|
// `shown` drives the slide/fade end-state.
|
|
58
59
|
const [rendered, setRendered] = useState(open);
|
|
59
60
|
const [shown, setShown] = useState(open);
|
|
60
|
-
const panelRef = useRef<HTMLElement | null>(null);
|
|
61
|
-
const onCloseRef = useRef(onClose);
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
onCloseRef.current = onClose;
|
|
65
|
-
}, [onClose]);
|
|
66
|
-
|
|
67
61
|
useEffect(() => {
|
|
68
62
|
if (open) {
|
|
69
63
|
queueMicrotask(() => setRendered(true));
|
|
@@ -82,101 +76,74 @@ export function SuiteMobileDrawer({
|
|
|
82
76
|
return () => window.clearTimeout(timeout);
|
|
83
77
|
}, [open, durationMs]);
|
|
84
78
|
|
|
85
|
-
// Escape to close, body scroll-lock, and focus inside while open.
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (!open) return;
|
|
88
|
-
|
|
89
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
90
|
-
if (event.key === "Escape") onCloseRef.current();
|
|
91
|
-
};
|
|
92
|
-
window.addEventListener("keydown", onKeyDown);
|
|
93
|
-
|
|
94
|
-
const previousOverflow = document.body.style.overflow;
|
|
95
|
-
document.body.style.overflow = "hidden";
|
|
96
|
-
|
|
97
|
-
const previouslyFocused =
|
|
98
|
-
document.activeElement instanceof HTMLElement
|
|
99
|
-
? document.activeElement
|
|
100
|
-
: null;
|
|
101
|
-
const frame = requestAnimationFrame(() => panelRef.current?.focus());
|
|
102
|
-
|
|
103
|
-
return () => {
|
|
104
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
105
|
-
document.body.style.overflow = previousOverflow;
|
|
106
|
-
cancelAnimationFrame(frame);
|
|
107
|
-
if (previouslyFocused?.isConnected) previouslyFocused.focus();
|
|
108
|
-
};
|
|
109
|
-
}, [open]);
|
|
110
|
-
|
|
111
79
|
if (!rendered) return null;
|
|
112
80
|
|
|
113
81
|
return (
|
|
114
|
-
<
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
aria-label={dialogLabel}
|
|
133
|
-
tabIndex={-1}
|
|
134
|
-
data-test={panelDataTest}
|
|
135
|
-
className={cn(
|
|
136
|
-
"relative flex h-dvh max-h-dvh w-[86%] max-w-72 flex-col overflow-hidden bg-ph-surface shadow-xl outline-none motion-safe:transition-transform motion-safe:ease-spring-fast",
|
|
137
|
-
side === "right" && "ml-auto",
|
|
138
|
-
shown
|
|
139
|
-
? "translate-x-0"
|
|
140
|
-
: side === "right"
|
|
141
|
-
? "translate-x-full"
|
|
142
|
-
: "-translate-x-full",
|
|
143
|
-
panelClassName,
|
|
144
|
-
)}
|
|
145
|
-
style={{ transitionDuration: `${durationMs}ms` }}
|
|
146
|
-
>
|
|
147
|
-
{showCloseButton ? (
|
|
148
|
-
<div
|
|
82
|
+
<Dialog.Root open={open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
|
|
83
|
+
<Dialog.Portal forceMount>
|
|
84
|
+
<Dialog.Overlay
|
|
85
|
+
forceMount
|
|
86
|
+
id={backdropId}
|
|
87
|
+
data-test={backdropDataTest ?? dataTest}
|
|
88
|
+
aria-label={backdropLabel}
|
|
89
|
+
className={cn(
|
|
90
|
+
"no-print fixed inset-0 z-50 bg-black/25 backdrop-blur-sm motion-safe:transition-opacity motion-safe:ease-spring-subtle lg:hidden",
|
|
91
|
+
shown ? "opacity-100" : "opacity-0",
|
|
92
|
+
)}
|
|
93
|
+
style={{ transitionDuration: `${durationMs}ms` }}
|
|
94
|
+
/>
|
|
95
|
+
<Dialog.Content forceMount asChild>
|
|
96
|
+
<aside
|
|
97
|
+
role="dialog"
|
|
98
|
+
data-test={panelDataTest}
|
|
99
|
+
aria-modal="true"
|
|
149
100
|
className={cn(
|
|
150
|
-
"flex h-
|
|
151
|
-
|
|
101
|
+
"fixed inset-y-0 left-0 z-50 flex h-dvh max-h-dvh w-[86%] max-w-72 flex-col overflow-hidden bg-ph-surface shadow-xl outline-none motion-safe:transition-transform motion-safe:ease-spring-fast lg:hidden",
|
|
102
|
+
side === "right" && "left-auto right-0",
|
|
103
|
+
shown
|
|
104
|
+
? "translate-x-0"
|
|
105
|
+
: side === "right"
|
|
106
|
+
? "translate-x-full"
|
|
107
|
+
: "-translate-x-full",
|
|
108
|
+
panelClassName,
|
|
152
109
|
)}
|
|
110
|
+
style={{ transitionDuration: `${durationMs}ms` }}
|
|
153
111
|
>
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
112
|
+
<Dialog.Title className="sr-only">{dialogLabel}</Dialog.Title>
|
|
113
|
+
{showCloseButton ? (
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
"flex h-14 shrink-0 items-center border-b border-ph-border px-3",
|
|
117
|
+
title ? "justify-between" : "justify-end",
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{title ? (
|
|
121
|
+
<span className="truncate text-sm font-semibold text-ph-ink">
|
|
122
|
+
{title}
|
|
123
|
+
</span>
|
|
124
|
+
) : null}
|
|
125
|
+
<Dialog.Close asChild>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
id={closeButtonId}
|
|
129
|
+
data-test={closeButtonDataTest}
|
|
130
|
+
className="-mr-1.5 inline-flex h-11 w-11 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
131
|
+
aria-label={closeLabel}
|
|
132
|
+
>
|
|
133
|
+
<X className="h-4 w-4" aria-hidden />
|
|
134
|
+
</button>
|
|
135
|
+
</Dialog.Close>
|
|
136
|
+
</div>
|
|
158
137
|
) : null}
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
data-test={closeButtonDataTest}
|
|
163
|
-
onClick={onClose}
|
|
164
|
-
className="-mr-1.5 inline-flex h-11 w-11 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
165
|
-
aria-label={closeLabel}
|
|
138
|
+
<div
|
|
139
|
+
data-test="suite-mobile-drawer-body"
|
|
140
|
+
className="suite-scroll-lock flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
|
166
141
|
>
|
|
167
|
-
|
|
168
|
-
</
|
|
169
|
-
</
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div
|
|
174
|
-
data-test="suite-mobile-drawer-body"
|
|
175
|
-
className="suite-scroll-lock flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
|
176
|
-
>
|
|
177
|
-
{children}
|
|
178
|
-
</div>
|
|
179
|
-
</aside>
|
|
180
|
-
</div>
|
|
142
|
+
{children}
|
|
143
|
+
</div>
|
|
144
|
+
</aside>
|
|
145
|
+
</Dialog.Content>
|
|
146
|
+
</Dialog.Portal>
|
|
147
|
+
</Dialog.Root>
|
|
181
148
|
);
|
|
182
149
|
}
|
|
@@ -5,7 +5,11 @@ import { useRouter } from "next/navigation";
|
|
|
5
5
|
import { Bell } from "iconoir-react";
|
|
6
6
|
|
|
7
7
|
import type { SuiteDropdownMenuComponent } from "../lib/injected";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
resolveSuiteNotificationHref,
|
|
10
|
+
suiteAppBaseUrl,
|
|
11
|
+
suiteAppSlugForNotificationSource,
|
|
12
|
+
} from "../lib/apps";
|
|
9
13
|
|
|
10
14
|
const POLL_MS = 60_000;
|
|
11
15
|
|
|
@@ -143,8 +147,30 @@ export function SuiteNotificationBell({
|
|
|
143
147
|
}
|
|
144
148
|
const target = resolveSuiteNotificationHref(n.href, n.source_app);
|
|
145
149
|
if (!target) return;
|
|
146
|
-
// Cross-app notifications must leave this product origin.
|
|
147
150
|
if (/^https?:\/\//i.test(target)) {
|
|
151
|
+
try {
|
|
152
|
+
const url = new URL(target);
|
|
153
|
+
const sourceApp = suiteAppSlugForNotificationSource(n.source_app);
|
|
154
|
+
const path = `${url.pathname}${url.search}${url.hash}`;
|
|
155
|
+
if (url.origin === window.location.origin) {
|
|
156
|
+
router.push(path);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (sourceApp && sourceApp !== "shellstack") {
|
|
160
|
+
const sourceOrigin = new URL(suiteAppBaseUrl(sourceApp)).origin;
|
|
161
|
+
if (url.origin === sourceOrigin) {
|
|
162
|
+
const launchUrl = new URL(
|
|
163
|
+
`/launch/${sourceApp}`,
|
|
164
|
+
suiteAppBaseUrl("shellstack"),
|
|
165
|
+
);
|
|
166
|
+
launchUrl.searchParams.set("next", path);
|
|
167
|
+
window.location.assign(launchUrl.toString());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Fall through to the original external link.
|
|
173
|
+
}
|
|
148
174
|
window.location.assign(target);
|
|
149
175
|
return;
|
|
150
176
|
}
|
|
@@ -184,7 +210,7 @@ export function SuiteNotificationBell({
|
|
|
184
210
|
<span className="relative inline-flex h-5 w-5">
|
|
185
211
|
<Bell className="h-5 w-5" strokeWidth={1.75} aria-hidden />
|
|
186
212
|
{unread > 0 ? (
|
|
187
|
-
<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-
|
|
213
|
+
<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-[var(--ph-on-accent)] ring-2 ring-ph-surface">
|
|
188
214
|
{unread > 9 ? "9+" : unread}
|
|
189
215
|
</span>
|
|
190
216
|
) : null}
|
|
@@ -215,7 +241,6 @@ export function SuiteNotificationBell({
|
|
|
215
241
|
items.map((n) => (
|
|
216
242
|
<button
|
|
217
243
|
key={n.id}
|
|
218
|
-
type="button"
|
|
219
244
|
id={`${dataTest}-item-${n.id}`}
|
|
220
245
|
data-test={`${dataTest}-item-${n.id}`}
|
|
221
246
|
onClick={() => void openItem(n)}
|
|
@@ -6,10 +6,7 @@ import { NavArrowLeft, Sparks } from "iconoir-react";
|
|
|
6
6
|
|
|
7
7
|
import { Tooltip } from "../../components/ui/Tooltip";
|
|
8
8
|
import { cn } from "../lib/cn";
|
|
9
|
-
import {
|
|
10
|
-
persistSuiteAskAiOpen,
|
|
11
|
-
SUITE_OPEN_ASK_AI_EVENT,
|
|
12
|
-
} from "./ai-panel";
|
|
9
|
+
import { persistSuiteAskAiOpen, SUITE_OPEN_ASK_AI_EVENT } from "./ai-panel";
|
|
13
10
|
import type { SuiteNavIcon } from "./suite-bottom-nav";
|
|
14
11
|
|
|
15
12
|
type CollapsedNode = ReactNode | ((collapsed: boolean) => ReactNode);
|
|
@@ -31,7 +28,7 @@ export interface SuiteSidebarProps {
|
|
|
31
28
|
/** Optional custom app switcher node, or a function of collapsed state. */
|
|
32
29
|
appSwitcher?: CollapsedNode;
|
|
33
30
|
/** Workspace/org/project switcher rendered below the app switcher. */
|
|
34
|
-
contextSwitcher
|
|
31
|
+
contextSwitcher?: CollapsedNode;
|
|
35
32
|
navItems: SuiteSidebarNavItem[];
|
|
36
33
|
/** Optional secondary nav / tree rendered below primary nav. */
|
|
37
34
|
secondaryNav?: ReactNode;
|
|
@@ -157,17 +154,15 @@ export function SuiteSidebar({
|
|
|
157
154
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
158
155
|
)}
|
|
159
156
|
>
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</button>
|
|
170
|
-
</Tooltip>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
data-test="suite-ask-ai-back"
|
|
160
|
+
aria-label="Back to navigation"
|
|
161
|
+
onClick={() => setAskAi(false)}
|
|
162
|
+
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"
|
|
163
|
+
>
|
|
164
|
+
<NavArrowLeft className="h-4 w-4" aria-hidden />
|
|
165
|
+
</button>
|
|
171
166
|
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-ph-ink">
|
|
172
167
|
Shelly AI
|
|
173
168
|
</span>
|
|
@@ -211,12 +206,19 @@ export function SuiteSidebar({
|
|
|
211
206
|
"flex min-h-0 flex-1 flex-col",
|
|
212
207
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
213
208
|
chatOpen ? "min-h-0" : "overflow-y-auto",
|
|
214
|
-
collapsed ? "px-1.5 py-2" : "p-
|
|
209
|
+
collapsed ? "px-1.5 py-2" : "p-3",
|
|
215
210
|
)}
|
|
216
211
|
>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
212
|
+
{contextSwitcher ? (
|
|
213
|
+
<div
|
|
214
|
+
className={cn(
|
|
215
|
+
"mb-4 w-full shrink-0",
|
|
216
|
+
collapsed && "flex justify-center",
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
{renderNode(contextSwitcher, collapsed)}
|
|
220
|
+
</div>
|
|
221
|
+
) : null}
|
|
220
222
|
|
|
221
223
|
{chatOpen ? (
|
|
222
224
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
@@ -238,8 +240,8 @@ export function SuiteSidebar({
|
|
|
238
240
|
</div>
|
|
239
241
|
) : (
|
|
240
242
|
<>
|
|
241
|
-
<nav aria-label="Pages" className="shrink-0 space-y-
|
|
242
|
-
<div className="space-y-
|
|
243
|
+
<nav aria-label="Pages" className="shrink-0 space-y-1">
|
|
244
|
+
<div className="space-y-1">
|
|
243
245
|
{navItems.map((item) => {
|
|
244
246
|
const Icon = item.icon;
|
|
245
247
|
const active = Boolean(item.active);
|
|
@@ -331,9 +333,7 @@ export function SuiteSidebar({
|
|
|
331
333
|
>
|
|
332
334
|
{renderNode(userMenu, collapsed)}
|
|
333
335
|
{showAskAi ? (
|
|
334
|
-
<div
|
|
335
|
-
className={cn("shrink-0", collapsed ? "mt-2" : "ml-auto")}
|
|
336
|
-
>
|
|
336
|
+
<div className={cn("shrink-0", collapsed ? "mt-2" : "ml-auto")}>
|
|
337
337
|
<Tooltip content="Shelly AI" side={collapsed ? "right" : "top"}>
|
|
338
338
|
{askAiButton}
|
|
339
339
|
</Tooltip>
|
|
@@ -167,14 +167,14 @@ export function SuiteEmptyState({
|
|
|
167
167
|
{icon}
|
|
168
168
|
</div>
|
|
169
169
|
) : null}
|
|
170
|
-
<
|
|
170
|
+
<h2
|
|
171
171
|
className={cn(
|
|
172
172
|
'font-semibold text-ph-ink',
|
|
173
173
|
size === 'sm' ? 'text-base' : 'text-lg',
|
|
174
174
|
)}
|
|
175
175
|
>
|
|
176
176
|
{title}
|
|
177
|
-
</
|
|
177
|
+
</h2>
|
|
178
178
|
{description ? (
|
|
179
179
|
<p className="mx-auto mt-2 max-w-sm text-sm leading-relaxed text-ph-subtle">
|
|
180
180
|
{description}
|
|
@@ -43,12 +43,12 @@ function computeInitials(
|
|
|
43
43
|
* email or name so avatars without a photo get a per-user colour instead of a
|
|
44
44
|
* flat grey. Uses theme `--ph-*` tokens so it adapts to light and dark themes.
|
|
45
45
|
*/
|
|
46
|
-
const AVATAR_TOKENS = [
|
|
47
|
-
"bg-ph-brand",
|
|
48
|
-
"bg-ph-violet",
|
|
49
|
-
"bg-ph-info",
|
|
50
|
-
"bg-ph-success",
|
|
51
|
-
"bg-ph-danger",
|
|
46
|
+
const AVATAR_TOKENS: Array<[string, string]> = [
|
|
47
|
+
["bg-ph-brand", "text-[var(--ph-on-accent)]"],
|
|
48
|
+
["bg-ph-violet", "text-[var(--ph-on-violet)]"],
|
|
49
|
+
["bg-ph-info", "text-[var(--ph-on-info)]"],
|
|
50
|
+
["bg-ph-success", "text-[var(--ph-on-success)]"],
|
|
51
|
+
["bg-ph-danger", "text-[var(--ph-on-danger)]"],
|
|
52
52
|
];
|
|
53
53
|
|
|
54
54
|
function avatarColorClass(seed: string): string {
|
|
@@ -56,7 +56,8 @@ function avatarColorClass(seed: string): string {
|
|
|
56
56
|
for (let i = 0; i < seed.length; i += 1) {
|
|
57
57
|
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
const [bg, ink] = AVATAR_TOKENS[Math.abs(hash) % AVATAR_TOKENS.length];
|
|
60
|
+
return `${bg} ${ink}`;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|
|
@@ -96,7 +97,7 @@ export function SuiteUserMenu({
|
|
|
96
97
|
) : (
|
|
97
98
|
<span
|
|
98
99
|
className={cn(
|
|
99
|
-
"flex h-full w-full items-center justify-center text-sm font-semibold
|
|
100
|
+
"flex h-full w-full items-center justify-center text-sm font-semibold",
|
|
100
101
|
fallbackColor,
|
|
101
102
|
)}
|
|
102
103
|
>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { cleanup, render, waitFor } from "@testing-library/react";
|
|
1
|
+
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
2
|
+
import { useEffect } from "react";
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { SUITE_THEME_COOKIE } from "../lib/theme-cookie";
|
|
6
|
+
import { suiteThemeBootScript } from "../lib/theme-boot";
|
|
7
|
+
import {
|
|
8
|
+
SuiteThemeProvider,
|
|
9
|
+
useSuiteTheme,
|
|
10
|
+
type SuiteThemeConfig,
|
|
11
|
+
type SuiteThemeContextValue,
|
|
12
|
+
} from "./theme-provider";
|
|
5
13
|
|
|
6
14
|
const config: SuiteThemeConfig = {
|
|
7
15
|
storageKey: "test-theme",
|
|
@@ -36,8 +44,8 @@ beforeEach(() => {
|
|
|
36
44
|
afterEach(() => {
|
|
37
45
|
cleanup();
|
|
38
46
|
vi.unstubAllGlobals();
|
|
47
|
+
document.cookie = `${SUITE_THEME_COOKIE}=; Path=/; Max-Age=0`;
|
|
39
48
|
document.documentElement.removeAttribute("data-theme");
|
|
40
|
-
document.documentElement.removeAttribute("data-suite-lock-page-zoom");
|
|
41
49
|
document.documentElement.classList.remove("dark");
|
|
42
50
|
document.getElementById("theme-color-meta")?.remove();
|
|
43
51
|
});
|
|
@@ -60,4 +68,80 @@ describe("SuiteThemeProvider", () => {
|
|
|
60
68
|
expect(meta).toHaveAttribute("content", config.themeColorDark),
|
|
61
69
|
);
|
|
62
70
|
});
|
|
71
|
+
|
|
72
|
+
it("adopts a shared-cookie preference from another suite app", async () => {
|
|
73
|
+
document.cookie = `${SUITE_THEME_COOKIE}=light; Path=/`;
|
|
74
|
+
document.documentElement.setAttribute("data-theme", config.darkThemeId);
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<SuiteThemeProvider config={config}>
|
|
78
|
+
<div />
|
|
79
|
+
</SuiteThemeProvider>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await waitFor(() =>
|
|
83
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe(
|
|
84
|
+
config.lightThemeId,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("persists setTheme to the shared cookie for other suite apps", async () => {
|
|
91
|
+
document.cookie = `${SUITE_THEME_COOKIE}=system; Path=/`;
|
|
92
|
+
document.documentElement.setAttribute("data-theme", config.lightThemeId);
|
|
93
|
+
|
|
94
|
+
let captured: SuiteThemeContextValue | null = null;
|
|
95
|
+
function Probe() {
|
|
96
|
+
const ctx = useSuiteTheme();
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
captured = ctx;
|
|
99
|
+
}, [ctx]);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
render(
|
|
104
|
+
<SuiteThemeProvider config={config}>
|
|
105
|
+
<Probe />
|
|
106
|
+
</SuiteThemeProvider>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await waitFor(() => expect(captured).not.toBeNull());
|
|
110
|
+
act(() => captured!.setTheme("dark"));
|
|
111
|
+
|
|
112
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe(
|
|
113
|
+
config.darkThemeId,
|
|
114
|
+
);
|
|
115
|
+
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
116
|
+
expect(document.cookie).toContain(`${SUITE_THEME_COOKIE}=dark`);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("suiteThemeBootScript", () => {
|
|
121
|
+
it("applies the shared cookie mode before the OS preference", () => {
|
|
122
|
+
document.cookie = `${SUITE_THEME_COOKIE}=light; Path=/`;
|
|
123
|
+
const script = suiteThemeBootScript({
|
|
124
|
+
lightThemeId: "x-light",
|
|
125
|
+
darkThemeId: "x-dark",
|
|
126
|
+
storageKey: "legacy",
|
|
127
|
+
});
|
|
128
|
+
new Function(script)();
|
|
129
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe(
|
|
130
|
+
"x-light",
|
|
131
|
+
);
|
|
132
|
+
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("falls back to the OS preference when no cookie or stored mode exists", () => {
|
|
136
|
+
const script = suiteThemeBootScript({
|
|
137
|
+
lightThemeId: "x-light",
|
|
138
|
+
darkThemeId: "x-dark",
|
|
139
|
+
storageKey: "legacy",
|
|
140
|
+
fallback: "light",
|
|
141
|
+
});
|
|
142
|
+
// The stubbed localStorage returns a legacy "dark" preference.
|
|
143
|
+
new Function(script)();
|
|
144
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("x-dark");
|
|
145
|
+
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
146
|
+
});
|
|
63
147
|
});
|