@xenide-io/the-old-ui-theme 0.9.18 → 0.11.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/dist/{Chip-BzuNgJIa.d.mts → Chip-Dw2g2t8D.d.mts} +18 -4
- package/dist/{Chip-BzuNgJIa.d.ts → Chip-Dw2g2t8D.d.ts} +18 -4
- package/dist/{chunk-6SVJAU5K.mjs → chunk-RIPGN6FG.mjs} +1333 -526
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +490 -119
- package/dist/index.mjs +11 -3
- package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
- package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
- package/dist/suite.d.mts +36 -4
- package/dist/suite.d.ts +36 -4
- package/dist/suite.js +4617 -4001
- package/dist/suite.mjs +1456 -868
- package/dist/tailwind-preset.js +10 -1
- package/dist/tailwind-preset.mjs +10 -1
- package/dist/ui.d.mts +33 -4
- package/dist/ui.d.ts +33 -4
- package/dist/ui.js +1364 -571
- package/dist/ui.mjs +15 -1
- package/docs/macos-identity-audit.md +279 -0
- package/package.json +1 -1
- package/src/app/globals.css +390 -44
- package/src/components/ui/Accordion.tsx +4 -7
- package/src/components/ui/Avatar.tsx +5 -5
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/Calendar.tsx +7 -7
- package/src/components/ui/CodeBlock.tsx +14 -11
- package/src/components/ui/CollapsibleSection.tsx +4 -4
- package/src/components/ui/CommandPalette.tsx +7 -10
- package/src/components/ui/EmptyState.tsx +2 -2
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Marketing.tsx +144 -0
- package/src/components/ui/MarketingIllustrations.tsx +287 -0
- package/src/components/ui/Panel.tsx +1 -1
- package/src/components/ui/SegmentedControl.tsx +59 -13
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/ShowcaseWrapper.tsx +3 -3
- package/src/components/ui/Table.tsx +45 -10
- package/src/components/ui/Typography.tsx +4 -4
- package/src/components/ui/avatar-stat.test.tsx +14 -1
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +19 -11
- package/src/components/ui/interaction-primitives.test.tsx +12 -6
- package/src/components/ui/marketing.test.tsx +39 -0
- package/src/components/ui/table.test.tsx +47 -0
- package/src/styles/suite-skin.css +471 -74
- package/src/styles/themes.css +45 -45
- package/src/styles/typography.css +25 -1
- package/src/suite/components/ai-draft-editor.tsx +184 -0
- package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
- package/src/suite/components/ai-panel.tsx +201 -31
- package/src/suite/components/app-switcher.tsx +22 -9
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-app-layout.tsx +7 -4
- package/src/suite/components/suite-bottom-nav.tsx +1 -1
- package/src/suite/components/suite-clients-projects-directory.tsx +6 -6
- package/src/suite/components/suite-integration-picker.tsx +7 -7
- package/src/suite/components/suite-integration-rules.tsx +5 -5
- package/src/suite/components/suite-layout.test.tsx +32 -3
- package/src/suite/components/suite-layout.tsx +4 -4
- package/src/suite/components/suite-mobile-drawer.tsx +2 -2
- package/src/suite/components/suite-mobile-header.tsx +1 -1
- package/src/suite/components/suite-notification-bell.tsx +27 -6
- package/src/suite/components/suite-notification-settings.tsx +182 -0
- package/src/suite/components/suite-palette-library.test.tsx +46 -0
- package/src/suite/components/suite-palette-library.tsx +82 -15
- package/src/suite/components/suite-settings-layout.tsx +2 -2
- package/src/suite/components/suite-settings-mobile-nav.tsx +10 -10
- package/src/suite/components/suite-sidebar.test.tsx +39 -0
- package/src/suite/components/suite-sidebar.tsx +41 -19
- package/src/suite/components/suite-skeleton.tsx +6 -6
- package/src/suite/components/suite-user-menu.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-calibrating.tsx +3 -3
- package/src/suite/components/today-ui.tsx +42 -37
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/index.ts +14 -0
- package/src/suite/lib/suite-notifications.tsx +5 -0
- package/tailwind-preset.ts +9 -0
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import {
|
|
4
4
|
useCallback,
|
|
5
5
|
useEffect,
|
|
6
|
+
useId,
|
|
6
7
|
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
type ReactNode,
|
|
11
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
10
12
|
} from 'react';
|
|
11
13
|
import { Filter, FilterList, NavArrowDown, Search } from 'iconoir-react';
|
|
12
14
|
import {
|
|
@@ -58,7 +60,7 @@ function FilterSelect({
|
|
|
58
60
|
aria-label={label}
|
|
59
61
|
value={value}
|
|
60
62
|
onChange={(event) => onChange(event.target.value)}
|
|
61
|
-
className="h-full w-full
|
|
63
|
+
className="ph-select h-full w-full py-2 pl-8 pr-7"
|
|
62
64
|
>
|
|
63
65
|
{children}
|
|
64
66
|
</select>
|
|
@@ -86,6 +88,8 @@ export function SuitePaletteLibrary({
|
|
|
86
88
|
const [libraryOpen, setLibraryOpen] = useState(false);
|
|
87
89
|
const [libraryQuery, setLibraryQuery] = useState('');
|
|
88
90
|
const libraryRef = useRef<HTMLDivElement>(null);
|
|
91
|
+
const libraryTriggerRef = useRef<HTMLButtonElement>(null);
|
|
92
|
+
const libraryListboxId = useId();
|
|
89
93
|
const [icons, setIcons] = useState<PaletteIcon[]>([]);
|
|
90
94
|
const [total, setTotal] = useState(0);
|
|
91
95
|
const [page, setPage] = useState(1);
|
|
@@ -245,6 +249,45 @@ export function SuitePaletteLibrary({
|
|
|
245
249
|
return () => document.removeEventListener('pointerdown', onPointerDown);
|
|
246
250
|
}, [libraryOpen]);
|
|
247
251
|
|
|
252
|
+
const closeLibrary = (restoreFocus = false) => {
|
|
253
|
+
setLibraryOpen(false);
|
|
254
|
+
if (restoreFocus) requestAnimationFrame(() => libraryTriggerRef.current?.focus());
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const handleLibraryOptionKeyDown = (
|
|
258
|
+
event: ReactKeyboardEvent<HTMLButtonElement>,
|
|
259
|
+
) => {
|
|
260
|
+
const options = Array.from(
|
|
261
|
+
libraryRef.current?.querySelectorAll<HTMLButtonElement>('[role="option"]') ?? [],
|
|
262
|
+
);
|
|
263
|
+
const index = options.indexOf(event.currentTarget);
|
|
264
|
+
let nextIndex: number;
|
|
265
|
+
|
|
266
|
+
switch (event.key) {
|
|
267
|
+
case 'ArrowDown':
|
|
268
|
+
nextIndex = (index + 1) % options.length;
|
|
269
|
+
break;
|
|
270
|
+
case 'ArrowUp':
|
|
271
|
+
nextIndex = (index - 1 + options.length) % options.length;
|
|
272
|
+
break;
|
|
273
|
+
case 'Home':
|
|
274
|
+
nextIndex = 0;
|
|
275
|
+
break;
|
|
276
|
+
case 'End':
|
|
277
|
+
nextIndex = options.length - 1;
|
|
278
|
+
break;
|
|
279
|
+
case 'Escape':
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
closeLibrary(true);
|
|
282
|
+
return;
|
|
283
|
+
default:
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
options[nextIndex]?.focus();
|
|
289
|
+
};
|
|
290
|
+
|
|
248
291
|
return (
|
|
249
292
|
<div className="space-y-3" data-test="palette-icon-library">
|
|
250
293
|
<div ref={libraryRef} className="space-y-2">
|
|
@@ -260,15 +303,23 @@ export function SuitePaletteLibrary({
|
|
|
260
303
|
onChange={(event) => setQuery(event.target.value)}
|
|
261
304
|
placeholder="Search icons — handshake, rocket, briefcase…"
|
|
262
305
|
aria-label="Search icons"
|
|
263
|
-
className="
|
|
306
|
+
className="ph-input w-full py-2 pl-8 pr-3"
|
|
264
307
|
/>
|
|
265
308
|
</div>
|
|
266
309
|
<button
|
|
310
|
+
ref={libraryTriggerRef}
|
|
267
311
|
type="button"
|
|
268
312
|
aria-haspopup="listbox"
|
|
269
313
|
aria-expanded={libraryOpen}
|
|
314
|
+
aria-controls={libraryOpen ? libraryListboxId : undefined}
|
|
270
315
|
onClick={() => setLibraryOpen((open) => !open)}
|
|
271
|
-
|
|
316
|
+
onKeyDown={(event) => {
|
|
317
|
+
if (event.key === 'Escape' && libraryOpen) {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
closeLibrary();
|
|
320
|
+
}
|
|
321
|
+
}}
|
|
322
|
+
className="ph-input flex items-center gap-2 px-2.5 py-2 text-sm text-ph-ink"
|
|
272
323
|
>
|
|
273
324
|
<FilterList className="h-4 w-4 shrink-0 text-ph-mutedtext" aria-hidden />
|
|
274
325
|
<span className="max-w-[10rem] truncate">
|
|
@@ -297,7 +348,7 @@ export function SuitePaletteLibrary({
|
|
|
297
348
|
</div>
|
|
298
349
|
|
|
299
350
|
{libraryOpen ? (
|
|
300
|
-
<div className="rounded-
|
|
351
|
+
<div className="rounded-[var(--ph-radius-overlay)] border border-[var(--ph-border-control)] bg-[var(--ph-surface-overlay)] p-2 shadow-[var(--ph-shadow-floating)]">
|
|
301
352
|
<div className="relative mb-2">
|
|
302
353
|
<Search
|
|
303
354
|
className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ph-mutedtext"
|
|
@@ -310,11 +361,23 @@ export function SuitePaletteLibrary({
|
|
|
310
361
|
onChange={(event) => setLibraryQuery(event.target.value)}
|
|
311
362
|
placeholder="Search libraries…"
|
|
312
363
|
aria-label="Search libraries"
|
|
313
|
-
className="w-full
|
|
364
|
+
className="ph-input w-full bg-[var(--ph-surface-inset)] py-1.5 pl-8 pr-3"
|
|
365
|
+
onKeyDown={(event) => {
|
|
366
|
+
if (event.key === 'ArrowDown') {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
libraryRef.current
|
|
369
|
+
?.querySelector<HTMLButtonElement>('[role="option"]')
|
|
370
|
+
?.focus();
|
|
371
|
+
} else if (event.key === 'Escape') {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
closeLibrary(true);
|
|
374
|
+
}
|
|
375
|
+
}}
|
|
314
376
|
/>
|
|
315
377
|
</div>
|
|
316
378
|
<ul
|
|
317
379
|
role="listbox"
|
|
380
|
+
id={libraryListboxId}
|
|
318
381
|
aria-label="Libraries"
|
|
319
382
|
className="max-h-56 overflow-y-auto"
|
|
320
383
|
>
|
|
@@ -323,15 +386,17 @@ export function SuitePaletteLibrary({
|
|
|
323
386
|
type="button"
|
|
324
387
|
role="option"
|
|
325
388
|
aria-selected={!collectionFilter}
|
|
389
|
+
tabIndex={-1}
|
|
390
|
+
onKeyDown={handleLibraryOptionKeyDown}
|
|
326
391
|
onClick={() => {
|
|
327
392
|
setCollectionFilter("");
|
|
328
|
-
|
|
393
|
+
closeLibrary(true);
|
|
329
394
|
}}
|
|
330
395
|
className={cn(
|
|
331
|
-
"flex w-full items-center rounded-
|
|
396
|
+
"flex w-full items-center rounded-[var(--ph-radius-control)] px-2 py-1.5 text-left text-sm focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)]",
|
|
332
397
|
!collectionFilter
|
|
333
|
-
? "bg-ph-
|
|
334
|
-
: "text-ph-ink hover:bg-ph-
|
|
398
|
+
? "bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)]"
|
|
399
|
+
: "text-ph-ink hover:bg-[var(--ph-surface-hover)]",
|
|
335
400
|
)}
|
|
336
401
|
>
|
|
337
402
|
All libraries
|
|
@@ -343,15 +408,17 @@ export function SuitePaletteLibrary({
|
|
|
343
408
|
type="button"
|
|
344
409
|
role="option"
|
|
345
410
|
aria-selected={collectionFilter === collection.slug}
|
|
411
|
+
tabIndex={-1}
|
|
412
|
+
onKeyDown={handleLibraryOptionKeyDown}
|
|
346
413
|
onClick={() => {
|
|
347
414
|
setCollectionFilter(collection.slug);
|
|
348
|
-
|
|
415
|
+
closeLibrary(true);
|
|
349
416
|
}}
|
|
350
417
|
className={cn(
|
|
351
|
-
"flex w-full items-center justify-between gap-2 rounded-
|
|
418
|
+
"flex w-full items-center justify-between gap-2 rounded-[var(--ph-radius-control)] px-2 py-1.5 text-left text-sm focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)]",
|
|
352
419
|
collectionFilter === collection.slug
|
|
353
|
-
? "bg-ph-
|
|
354
|
-
: "text-ph-ink hover:bg-ph-
|
|
420
|
+
? "bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)]"
|
|
421
|
+
: "text-ph-ink hover:bg-[var(--ph-surface-hover)]",
|
|
355
422
|
)}
|
|
356
423
|
>
|
|
357
424
|
<span className="truncate">{collection.name}</span>
|
|
@@ -381,7 +448,7 @@ export function SuitePaletteLibrary({
|
|
|
381
448
|
</p>
|
|
382
449
|
) : (
|
|
383
450
|
<>
|
|
384
|
-
<div className="max-h-[320px] overflow-y-auto rounded-
|
|
451
|
+
<div className="max-h-[320px] overflow-y-auto rounded-[var(--ph-radius-surface)] border border-ph-border p-2 focus-within:border-[var(--ph-selected-border)]">
|
|
385
452
|
<div className="grid grid-cols-6 gap-1.5 sm:grid-cols-8">
|
|
386
453
|
{icons.map((icon) => (
|
|
387
454
|
<button
|
|
@@ -390,7 +457,7 @@ export function SuitePaletteLibrary({
|
|
|
390
457
|
onClick={() => onPick({ url: icon.url, name: icon.name })}
|
|
391
458
|
title={icon.name}
|
|
392
459
|
aria-label={`Use ${icon.name}`}
|
|
393
|
-
className="flex h-10 items-center justify-center rounded-
|
|
460
|
+
className="flex h-10 items-center justify-center rounded-[var(--ph-radius-control)] border border-transparent p-1.5 hover:border-[var(--ph-selected-border)] hover:bg-[var(--ph-surface-hover)] focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)]"
|
|
394
461
|
>
|
|
395
462
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
396
463
|
<img
|
|
@@ -53,7 +53,7 @@ export function SuiteSettingsLayout({
|
|
|
53
53
|
{loading ?? (
|
|
54
54
|
<div className="suite-page-body mt-6 flex min-h-0 flex-1 flex-col">
|
|
55
55
|
{mobileNav ? (
|
|
56
|
-
<div className="sticky top-0 z-20 mb-2 shrink-0
|
|
56
|
+
<div className="suite-settings-mobile-bar sticky top-0 z-20 mb-2 shrink-0 lg:hidden">
|
|
57
57
|
{mobileNav}
|
|
58
58
|
</div>
|
|
59
59
|
) : null}
|
|
@@ -61,7 +61,7 @@ export function SuiteSettingsLayout({
|
|
|
61
61
|
<div className="flex min-h-0 flex-1 flex-col gap-6 lg:flex-row lg:gap-8">
|
|
62
62
|
<aside
|
|
63
63
|
aria-label="Settings"
|
|
64
|
-
className="hidden w-56 shrink-0
|
|
64
|
+
className="suite-settings-sidebar hidden w-56 shrink-0 lg:block lg:self-start lg:max-h-full lg:overflow-y-auto lg:overscroll-contain"
|
|
65
65
|
>
|
|
66
66
|
{sidebar}
|
|
67
67
|
</aside>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { isValidElement, type ElementType, type ReactNode } from "react";
|
|
4
|
+
import { trackSuiteEvent } from "../../lib/suite-analytics";
|
|
4
5
|
import { cn } from "../lib/cn";
|
|
5
6
|
|
|
6
7
|
export interface SuiteSettingsNavItem {
|
|
@@ -50,8 +51,8 @@ export function SuiteSettingsMobileNav({
|
|
|
50
51
|
) : (
|
|
51
52
|
<Icon
|
|
52
53
|
className={cn(
|
|
53
|
-
"h-4 w-4 shrink-0 transition",
|
|
54
|
-
active ? "text-ph-
|
|
54
|
+
"h-4 w-4 shrink-0 transition-colors",
|
|
55
|
+
active ? "text-[var(--ph-selected-text)]" : "text-ph-mutedtext",
|
|
55
56
|
)}
|
|
56
57
|
aria-hidden
|
|
57
58
|
/>
|
|
@@ -62,21 +63,20 @@ export function SuiteSettingsMobileNav({
|
|
|
62
63
|
type="button"
|
|
63
64
|
id={item.id}
|
|
64
65
|
data-test={item.testId}
|
|
65
|
-
onClick={() =>
|
|
66
|
+
onClick={() => {
|
|
67
|
+
trackSuiteEvent("Tab Viewed", { tab: item.id ?? item.href });
|
|
68
|
+
onSelect(item.href);
|
|
69
|
+
}}
|
|
66
70
|
aria-current={active ? "page" : undefined}
|
|
67
71
|
className={cn(
|
|
68
|
-
"group relative flex shrink-0 snap-start items-center gap-2 rounded-
|
|
69
|
-
"min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand",
|
|
72
|
+
"group relative flex min-h-[44px] shrink-0 snap-start items-center gap-2 rounded-[var(--ph-radius-control)] border px-3.5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)]",
|
|
70
73
|
active
|
|
71
|
-
? "bg-ph-
|
|
72
|
-
: "bg-ph-surface text-ph-subtle
|
|
74
|
+
? "border-[var(--ph-selected-border)] bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)]"
|
|
75
|
+
: "border-ph-border bg-ph-surface text-ph-subtle hover:bg-[var(--ph-surface-hover)] hover:text-ph-ink",
|
|
73
76
|
)}
|
|
74
77
|
>
|
|
75
78
|
{iconNode}
|
|
76
79
|
<span className="whitespace-nowrap">{item.label}</span>
|
|
77
|
-
{active ? (
|
|
78
|
-
<span className="absolute inset-x-0 -bottom-3 h-0.5 rounded-full bg-ph-ink lg:hidden" />
|
|
79
|
-
) : null}
|
|
80
80
|
</button>
|
|
81
81
|
);
|
|
82
82
|
})}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { SuiteSidebar } from "./suite-sidebar";
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
describe("SuiteSidebar Ask AI", () => {
|
|
11
|
+
it("moves focus into chat and restores it after Escape", async () => {
|
|
12
|
+
const user = userEvent.setup();
|
|
13
|
+
|
|
14
|
+
function Fixture() {
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
return (
|
|
17
|
+
<SuiteSidebar
|
|
18
|
+
navItems={[]}
|
|
19
|
+
userMenu={<span>Account</span>}
|
|
20
|
+
askAiOpen={open}
|
|
21
|
+
onAskAiOpenChange={setOpen}
|
|
22
|
+
askAi={<div>Chat</div>}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(<Fixture />);
|
|
28
|
+
const trigger = screen.getByRole("button", { name: "Shelly AI" });
|
|
29
|
+
await user.click(trigger);
|
|
30
|
+
|
|
31
|
+
const back = screen.getByRole("button", { name: "Back to navigation" });
|
|
32
|
+
await waitFor(() => expect(back).toHaveFocus());
|
|
33
|
+
await user.keyboard("{Escape}");
|
|
34
|
+
|
|
35
|
+
await waitFor(() =>
|
|
36
|
+
expect(screen.getByRole("button", { name: "Shelly AI" })).toHaveFocus(),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { useCallback, useEffect, type ReactNode } from "react";
|
|
4
|
+
import { useCallback, useEffect, useRef, type ReactNode } from "react";
|
|
5
5
|
import { NavArrowLeft, Sparks } from "iconoir-react";
|
|
6
6
|
|
|
7
7
|
import { Tooltip } from "../../components/ui/Tooltip";
|
|
@@ -83,6 +83,9 @@ export function SuiteSidebar({
|
|
|
83
83
|
}: SuiteSidebarProps) {
|
|
84
84
|
const showAskAi = Boolean(onAskAiOpenChange && askAi);
|
|
85
85
|
const chatOpen = showAskAi && askAiOpen && !collapsed;
|
|
86
|
+
const askAiButtonRef = useRef<HTMLButtonElement>(null);
|
|
87
|
+
const askAiBackRef = useRef<HTMLButtonElement>(null);
|
|
88
|
+
const restoreAskAiFocusRef = useRef(false);
|
|
86
89
|
|
|
87
90
|
const setAskAi = useCallback(
|
|
88
91
|
(next: boolean) => {
|
|
@@ -105,15 +108,35 @@ export function SuiteSidebar({
|
|
|
105
108
|
useEffect(() => {
|
|
106
109
|
if (!chatOpen) return;
|
|
107
110
|
function onKey(event: KeyboardEvent) {
|
|
108
|
-
if (event.key === "Escape")
|
|
111
|
+
if (event.key === "Escape") {
|
|
112
|
+
restoreAskAiFocusRef.current = true;
|
|
113
|
+
setAskAi(false);
|
|
114
|
+
}
|
|
109
115
|
}
|
|
110
116
|
window.addEventListener("keydown", onKey);
|
|
111
117
|
return () => window.removeEventListener("keydown", onKey);
|
|
112
118
|
}, [chatOpen, setAskAi]);
|
|
113
119
|
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const frame = requestAnimationFrame(() => {
|
|
122
|
+
if (chatOpen) {
|
|
123
|
+
askAiBackRef.current?.focus();
|
|
124
|
+
} else if (restoreAskAiFocusRef.current) {
|
|
125
|
+
restoreAskAiFocusRef.current = false;
|
|
126
|
+
askAiButtonRef.current?.focus();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return () => cancelAnimationFrame(frame);
|
|
130
|
+
}, [chatOpen]);
|
|
131
|
+
|
|
132
|
+
function closeAskAi() {
|
|
133
|
+
restoreAskAiFocusRef.current = true;
|
|
134
|
+
setAskAi(false);
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
function toggleAskAi() {
|
|
115
138
|
if (askAiOpen) {
|
|
116
|
-
|
|
139
|
+
closeAskAi();
|
|
117
140
|
return;
|
|
118
141
|
}
|
|
119
142
|
if (collapsed) onRequestExpand?.();
|
|
@@ -122,13 +145,14 @@ export function SuiteSidebar({
|
|
|
122
145
|
|
|
123
146
|
const askAiButton = (
|
|
124
147
|
<button
|
|
148
|
+
ref={askAiButtonRef}
|
|
125
149
|
type="button"
|
|
126
150
|
data-test="suite-ask-ai-toggle"
|
|
127
151
|
aria-pressed={askAiOpen}
|
|
128
152
|
aria-label={askAiOpen ? "Back to navigation" : "Shelly AI"}
|
|
129
153
|
onClick={toggleAskAi}
|
|
130
154
|
className={cn(
|
|
131
|
-
"
|
|
155
|
+
"ph-btn ph-btn-ghost ph-btn-square h-10 w-10 shrink-0",
|
|
132
156
|
askAiOpen
|
|
133
157
|
? "bg-ph-brand/10 text-ph-brand"
|
|
134
158
|
: "text-ph-mutedtext hover:bg-ph-muted hover:text-ph-ink",
|
|
@@ -142,9 +166,9 @@ export function SuiteSidebar({
|
|
|
142
166
|
return (
|
|
143
167
|
<div
|
|
144
168
|
data-suite-chat-open=""
|
|
169
|
+
data-surface={surface ? "true" : "false"}
|
|
145
170
|
className={cn(
|
|
146
|
-
"flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
147
|
-
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
171
|
+
"suite-sidebar flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
148
172
|
className,
|
|
149
173
|
)}
|
|
150
174
|
>
|
|
@@ -155,11 +179,12 @@ export function SuiteSidebar({
|
|
|
155
179
|
)}
|
|
156
180
|
>
|
|
157
181
|
<button
|
|
182
|
+
ref={askAiBackRef}
|
|
158
183
|
type="button"
|
|
159
184
|
data-test="suite-ask-ai-back"
|
|
160
185
|
aria-label="Back to navigation"
|
|
161
|
-
onClick={
|
|
162
|
-
className="
|
|
186
|
+
onClick={closeAskAi}
|
|
187
|
+
className="ph-btn ph-btn-ghost ph-btn-square h-10 w-10 shrink-0 text-ph-subtle"
|
|
163
188
|
>
|
|
164
189
|
<NavArrowLeft className="h-4 w-4" aria-hidden />
|
|
165
190
|
</button>
|
|
@@ -174,9 +199,9 @@ export function SuiteSidebar({
|
|
|
174
199
|
|
|
175
200
|
return (
|
|
176
201
|
<div
|
|
202
|
+
data-surface={surface ? "true" : "false"}
|
|
177
203
|
className={cn(
|
|
178
|
-
"flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
179
|
-
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
204
|
+
"suite-sidebar flex h-full w-full min-w-0 flex-col text-ph-ink",
|
|
180
205
|
className,
|
|
181
206
|
)}
|
|
182
207
|
>
|
|
@@ -184,7 +209,6 @@ export function SuiteSidebar({
|
|
|
184
209
|
<div
|
|
185
210
|
className={cn(
|
|
186
211
|
"flex shrink-0 border-b border-ph-border",
|
|
187
|
-
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
188
212
|
collapsed
|
|
189
213
|
? "flex-col items-center gap-1 px-1 py-2"
|
|
190
214
|
: "h-14 items-center justify-between gap-2 px-3",
|
|
@@ -204,7 +228,6 @@ export function SuiteSidebar({
|
|
|
204
228
|
<div
|
|
205
229
|
className={cn(
|
|
206
230
|
"flex min-h-0 flex-1 flex-col",
|
|
207
|
-
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
208
231
|
chatOpen ? "min-h-0" : "overflow-y-auto",
|
|
209
232
|
collapsed ? "px-1.5 py-2" : "p-3",
|
|
210
233
|
)}
|
|
@@ -226,8 +249,8 @@ export function SuiteSidebar({
|
|
|
226
249
|
<button
|
|
227
250
|
type="button"
|
|
228
251
|
data-test="suite-ask-ai-back"
|
|
229
|
-
onClick={
|
|
230
|
-
className="
|
|
252
|
+
onClick={closeAskAi}
|
|
253
|
+
className="ph-btn ph-btn-ghost ph-btn-sm h-9 text-ph-subtle"
|
|
231
254
|
>
|
|
232
255
|
<NavArrowLeft className="h-4 w-4" aria-hidden />
|
|
233
256
|
Nav
|
|
@@ -254,10 +277,10 @@ export function SuiteSidebar({
|
|
|
254
277
|
aria-current={active ? "page" : undefined}
|
|
255
278
|
onClick={item.onClick}
|
|
256
279
|
className={cn(
|
|
257
|
-
"group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-
|
|
280
|
+
"suite-sidebar-link group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-control)] text-sm font-medium transition-colors",
|
|
258
281
|
active
|
|
259
|
-
? "bg-ph-
|
|
260
|
-
: "text-ph-subtle hover:bg-ph-
|
|
282
|
+
? "bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)]"
|
|
283
|
+
: "text-ph-subtle hover:bg-[var(--ph-surface-hover)] hover:text-ph-ink",
|
|
261
284
|
collapsed
|
|
262
285
|
? "mx-auto size-11 shrink-0 justify-center gap-0 px-0 py-0"
|
|
263
286
|
: "w-full px-2.5 py-2",
|
|
@@ -279,7 +302,7 @@ export function SuiteSidebar({
|
|
|
279
302
|
className={cn(
|
|
280
303
|
"relative truncate",
|
|
281
304
|
active
|
|
282
|
-
? "text-ph-
|
|
305
|
+
? "text-[var(--ph-selected-text)]"
|
|
283
306
|
: "text-ph-subtle group-hover:text-ph-ink",
|
|
284
307
|
collapsed && "sr-only",
|
|
285
308
|
)}
|
|
@@ -319,7 +342,6 @@ export function SuiteSidebar({
|
|
|
319
342
|
<div
|
|
320
343
|
className={cn(
|
|
321
344
|
"shrink-0 border-t border-ph-border",
|
|
322
|
-
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
323
345
|
collapsed ? "p-1.5" : "p-3",
|
|
324
346
|
)}
|
|
325
347
|
>
|
|
@@ -65,7 +65,7 @@ export function SuiteSkeletonCard({
|
|
|
65
65
|
return (
|
|
66
66
|
<div
|
|
67
67
|
className={cn(
|
|
68
|
-
'ph-skeleton-card animate-[skeleton-stagger_0.5s_ease-out_both] rounded-
|
|
68
|
+
'ph-skeleton-card animate-[skeleton-stagger_0.5s_ease-out_both] rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface p-6',
|
|
69
69
|
className,
|
|
70
70
|
)}
|
|
71
71
|
aria-hidden
|
|
@@ -75,7 +75,7 @@ export function SuiteSkeletonCard({
|
|
|
75
75
|
className="mb-5 flex items-center gap-3 animate-[skeleton-stagger_0.5s_ease-out_both]"
|
|
76
76
|
style={{ animationDelay: '100ms' }}
|
|
77
77
|
>
|
|
78
|
-
<div className="ph-skeleton h-10 w-10 rounded-
|
|
78
|
+
<div className="ph-skeleton h-10 w-10 rounded-[var(--ph-radius-control)]" />
|
|
79
79
|
<div className="flex-1 space-y-2">
|
|
80
80
|
<div className="ph-skeleton h-4 w-1/3" />
|
|
81
81
|
<div className="ph-skeleton h-3 w-1/2" />
|
|
@@ -115,8 +115,8 @@ export function SuiteSkeletonList({
|
|
|
115
115
|
className="animate-[skeleton-stagger_0.6s_ease-out_both]"
|
|
116
116
|
style={{ animationDelay: `${index * 100}ms` }}
|
|
117
117
|
>
|
|
118
|
-
<div className="flex items-center gap-3 rounded-
|
|
119
|
-
<div className="ph-skeleton h-10 w-10 rounded-
|
|
118
|
+
<div className="flex items-center gap-3 rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface p-4">
|
|
119
|
+
<div className="ph-skeleton h-10 w-10 rounded-[var(--ph-radius-control)]" />
|
|
120
120
|
<div className="min-w-0 flex-1 space-y-2">
|
|
121
121
|
<div className="ph-skeleton h-3.5 w-3/4" />
|
|
122
122
|
<div className="ph-skeleton h-3 w-1/2" />
|
|
@@ -152,7 +152,7 @@ export function SuiteEmptyState({
|
|
|
152
152
|
return (
|
|
153
153
|
<div
|
|
154
154
|
className={cn(
|
|
155
|
-
'relative overflow-hidden rounded-
|
|
155
|
+
'relative overflow-hidden rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface text-center',
|
|
156
156
|
size === 'sm' ? 'px-5 py-8' : 'px-6 py-12 sm:px-10 sm:py-16',
|
|
157
157
|
className,
|
|
158
158
|
)}
|
|
@@ -163,7 +163,7 @@ export function SuiteEmptyState({
|
|
|
163
163
|
</div>
|
|
164
164
|
<div className="relative">
|
|
165
165
|
{icon ? (
|
|
166
|
-
<div className="mx-auto mb-4 inline-flex items-center justify-center rounded-
|
|
166
|
+
<div className="mx-auto mb-4 inline-flex items-center justify-center rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-muted p-3 shadow-[var(--ph-shadow-control)]">
|
|
167
167
|
{icon}
|
|
168
168
|
</div>
|
|
169
169
|
) : null}
|
|
@@ -148,7 +148,7 @@ export function SuiteUserMenu({
|
|
|
148
148
|
className={cn(
|
|
149
149
|
"[&_.ph-dropdown-trigger]:rounded-full",
|
|
150
150
|
showDetails &&
|
|
151
|
-
"min-w-0 flex-1 [&_.ph-dropdown-trigger]:w-full [&_.ph-dropdown-trigger]:justify-start [&_.ph-dropdown-trigger]:rounded-
|
|
151
|
+
"min-w-0 flex-1 [&_.ph-dropdown-trigger]:w-full [&_.ph-dropdown-trigger]:justify-start [&_.ph-dropdown-trigger]:rounded-[var(--ph-radius-control)]",
|
|
152
152
|
showSignOutAction ? undefined : className,
|
|
153
153
|
)}
|
|
154
154
|
data-test={dataTest}
|
|
@@ -193,7 +193,7 @@ export function SuiteUserMenu({
|
|
|
193
193
|
data-test={`${dataTest}-visible-sign-out`}
|
|
194
194
|
title="Sign out"
|
|
195
195
|
aria-label="Sign out"
|
|
196
|
-
className="
|
|
196
|
+
className="ph-btn ph-btn-ghost ph-btn-square h-10 w-10 shrink-0 text-ph-mutedtext"
|
|
197
197
|
onClick={onSignOut}
|
|
198
198
|
>
|
|
199
199
|
<LogOut className="h-4 w-4" aria-hidden />
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import { readCookie, writeCookie } from '../lib/cookies';
|
|
15
15
|
import { SUITE_THEME_CHANNEL, SUITE_THEME_COOKIE } from '../lib/theme-cookie';
|
|
16
|
+
import { trackSuiteEvent } from '../../lib/suite-analytics';
|
|
16
17
|
|
|
17
18
|
export type SuiteTheme = 'system' | 'light' | 'dark';
|
|
18
19
|
export type SuiteResolvedTheme = 'light' | 'dark';
|
|
@@ -271,6 +272,7 @@ export function SuiteThemeProvider({
|
|
|
271
272
|
}
|
|
272
273
|
writeSharedTheme(next);
|
|
273
274
|
broadcastTheme(next);
|
|
275
|
+
trackSuiteEvent('Theme Updated', { theme: next, scope: 'suite' });
|
|
274
276
|
},
|
|
275
277
|
[config],
|
|
276
278
|
);
|
|
@@ -30,7 +30,7 @@ export function TodayCalibrating({
|
|
|
30
30
|
return (
|
|
31
31
|
<div
|
|
32
32
|
className={cn(
|
|
33
|
-
'suite-auth-handoff relative flex min-h-[14rem] flex-col items-center justify-center overflow-hidden rounded-
|
|
33
|
+
'suite-auth-handoff relative flex min-h-[14rem] flex-col items-center justify-center overflow-hidden rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface px-5 py-8 text-center',
|
|
34
34
|
className,
|
|
35
35
|
)}
|
|
36
36
|
data-test="today-calibrating"
|
|
@@ -39,7 +39,7 @@ export function TodayCalibrating({
|
|
|
39
39
|
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(251,191,36,0.1),transparent_55%)]"
|
|
40
40
|
aria-hidden
|
|
41
41
|
/>
|
|
42
|
-
<div className="relative mb-5 flex h-14 w-14 items-center justify-center rounded-
|
|
42
|
+
<div className="relative mb-5 flex h-14 w-14 items-center justify-center rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface text-ph-ink shadow-[var(--ph-shadow-control)]">
|
|
43
43
|
<Sun className="h-8 w-8" strokeWidth={2} aria-hidden />
|
|
44
44
|
</div>
|
|
45
45
|
<h2 className="relative text-base font-semibold tracking-tight text-ph-ink sm:text-lg md:text-xl">
|
|
@@ -56,7 +56,7 @@ export function TodayCalibrating({
|
|
|
56
56
|
type="button"
|
|
57
57
|
onClick={onRetry}
|
|
58
58
|
disabled={retrying}
|
|
59
|
-
className="
|
|
59
|
+
className="ph-btn ph-btn-secondary relative mt-6"
|
|
60
60
|
>
|
|
61
61
|
{retrying ? (
|
|
62
62
|
<Spinner size="sm" className={retrySpinnerClassName} />
|