@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
|
@@ -166,6 +166,7 @@ export function SuiteAiPanel({
|
|
|
166
166
|
onSwitchApp,
|
|
167
167
|
onSameAppNavigate,
|
|
168
168
|
resolveAction,
|
|
169
|
+
isWaitingForReply = false,
|
|
169
170
|
}: {
|
|
170
171
|
presets?: SuiteAiPreset[];
|
|
171
172
|
emptyState?: ReactNode;
|
|
@@ -173,6 +174,7 @@ export function SuiteAiPanel({
|
|
|
173
174
|
fetchChat: () => Promise<{
|
|
174
175
|
messages: SuiteAiChatMessage[];
|
|
175
176
|
configured: boolean;
|
|
177
|
+
jobs?: Array<{ status?: string; error?: string }>;
|
|
176
178
|
}>;
|
|
177
179
|
sendMessage: (params: {
|
|
178
180
|
prompt: string;
|
|
@@ -193,6 +195,8 @@ export function SuiteAiPanel({
|
|
|
193
195
|
id: string,
|
|
194
196
|
decision: "confirm" | "cancel",
|
|
195
197
|
) => Promise<{ messages: SuiteAiChatMessage[] }>;
|
|
198
|
+
/** A durable background job is still preparing the assistant reply. */
|
|
199
|
+
isWaitingForReply?: boolean;
|
|
196
200
|
}) {
|
|
197
201
|
const inputId = useId();
|
|
198
202
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
@@ -216,6 +220,7 @@ export function SuiteAiPanel({
|
|
|
216
220
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
217
221
|
const abortRef = useRef<AbortController | null>(null);
|
|
218
222
|
const pendingPromptRef = useRef<string | null>(null);
|
|
223
|
+
const waiting = loading || isWaitingForReply;
|
|
219
224
|
|
|
220
225
|
useEffect(() => {
|
|
221
226
|
function onOpen(event: Event) {
|
|
@@ -253,6 +258,16 @@ export function SuiteAiPanel({
|
|
|
253
258
|
setMessages(data.messages ?? []);
|
|
254
259
|
if (!data.configured) {
|
|
255
260
|
setError("ShellStack AI is not configured for this workspace yet.");
|
|
261
|
+
} else {
|
|
262
|
+
const latestJob = data.jobs?.[0];
|
|
263
|
+
if (
|
|
264
|
+
latestJob?.status === "failed" &&
|
|
265
|
+
latestJob.error &&
|
|
266
|
+
latestJob.error !== "Chat cleared." &&
|
|
267
|
+
data.messages.at(-1)?.role !== "assistant"
|
|
268
|
+
) {
|
|
269
|
+
setError(latestJob.error);
|
|
270
|
+
}
|
|
256
271
|
}
|
|
257
272
|
} catch (err) {
|
|
258
273
|
setError(
|
|
@@ -276,7 +291,7 @@ export function SuiteAiPanel({
|
|
|
276
291
|
|
|
277
292
|
useEffect(() => {
|
|
278
293
|
if (open && atBottom) scrollToBottom();
|
|
279
|
-
}, [messages,
|
|
294
|
+
}, [messages, waiting, open, atBottom, scrollToBottom]);
|
|
280
295
|
|
|
281
296
|
useEffect(() => {
|
|
282
297
|
if (open && !hydrating) textareaRef.current?.focus();
|
|
@@ -291,7 +306,7 @@ export function SuiteAiPanel({
|
|
|
291
306
|
|
|
292
307
|
async function ask(text: string) {
|
|
293
308
|
const trimmed = text.trim();
|
|
294
|
-
if (!trimmed ||
|
|
309
|
+
if (!trimmed || waiting) return;
|
|
295
310
|
const controller = new AbortController();
|
|
296
311
|
abortRef.current = controller;
|
|
297
312
|
setLoading(true);
|
|
@@ -424,7 +439,7 @@ export function SuiteAiPanel({
|
|
|
424
439
|
<button
|
|
425
440
|
type="button"
|
|
426
441
|
onClick={() => void handleClear()}
|
|
427
|
-
disabled={
|
|
442
|
+
disabled={waiting}
|
|
428
443
|
className="ml-auto inline-flex h-9 items-center gap-1.5 rounded-lg px-2 text-xs font-medium text-ph-subtle transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
|
|
429
444
|
data-test="ask-ai-clear"
|
|
430
445
|
>
|
|
@@ -469,7 +484,7 @@ export function SuiteAiPanel({
|
|
|
469
484
|
key={preset.label}
|
|
470
485
|
type="button"
|
|
471
486
|
onClick={() => void ask(preset.prompt)}
|
|
472
|
-
disabled={
|
|
487
|
+
disabled={waiting || hydrating}
|
|
473
488
|
className="group flex w-full items-center gap-2 rounded-xl border border-ph-border bg-ph-surface px-3 py-2 text-left text-sm text-ph-ink transition-colors hover:border-ph-brand/40 hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
|
|
474
489
|
>
|
|
475
490
|
<Sparks className="h-4 w-4 shrink-0 text-ph-brand" />
|
|
@@ -515,9 +530,16 @@ export function SuiteAiPanel({
|
|
|
515
530
|
<div className="rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink">
|
|
516
531
|
<Suspense
|
|
517
532
|
fallback={
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
|
|
533
|
+
<div
|
|
534
|
+
className="space-y-2 py-0.5"
|
|
535
|
+
role="status"
|
|
536
|
+
aria-label="Rendering response"
|
|
537
|
+
>
|
|
538
|
+
<span className="sr-only">Rendering response…</span>
|
|
539
|
+
<div className="h-3 w-11/12 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
540
|
+
<div className="h-3 w-full rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
541
|
+
<div className="h-3 w-3/5 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
542
|
+
</div>
|
|
521
543
|
}
|
|
522
544
|
>
|
|
523
545
|
<SuiteAiBlockNoteMessage markdown={message.content} />
|
|
@@ -584,7 +606,7 @@ export function SuiteAiPanel({
|
|
|
584
606
|
);
|
|
585
607
|
})}
|
|
586
608
|
|
|
587
|
-
{
|
|
609
|
+
{waiting ? (
|
|
588
610
|
<div className="flex items-end gap-2" aria-hidden>
|
|
589
611
|
<span className="mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand">
|
|
590
612
|
<Icon className="h-3.5 w-3.5" />
|
|
@@ -597,7 +619,7 @@ export function SuiteAiPanel({
|
|
|
597
619
|
</div>
|
|
598
620
|
) : null}
|
|
599
621
|
|
|
600
|
-
{
|
|
622
|
+
{waiting ? (
|
|
601
623
|
<span className="sr-only" aria-live="polite">
|
|
602
624
|
Assistant is responding…
|
|
603
625
|
</span>
|
|
@@ -615,7 +637,7 @@ export function SuiteAiPanel({
|
|
|
615
637
|
<button
|
|
616
638
|
type="button"
|
|
617
639
|
onClick={retry}
|
|
618
|
-
disabled={
|
|
640
|
+
disabled={waiting}
|
|
619
641
|
className="mt-2 inline-flex min-h-8 items-center gap-1.5 rounded-lg border border-ph-border bg-ph-surface px-2.5 text-xs font-medium text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
|
|
620
642
|
>
|
|
621
643
|
Try again
|
|
@@ -678,6 +700,15 @@ export function SuiteAiPanel({
|
|
|
678
700
|
<Square className="h-3.5 w-3.5 fill-current" />
|
|
679
701
|
</button>
|
|
680
702
|
</Tooltip>
|
|
703
|
+
) : isWaitingForReply ? (
|
|
704
|
+
<span
|
|
705
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-muted text-ph-subtle"
|
|
706
|
+
data-test="ask-ai-waiting"
|
|
707
|
+
role="status"
|
|
708
|
+
aria-label="Shelly is responding"
|
|
709
|
+
>
|
|
710
|
+
<Spinner className="h-4 w-4 animate-spin" />
|
|
711
|
+
</span>
|
|
681
712
|
) : (
|
|
682
713
|
<Tooltip content="Send message">
|
|
683
714
|
<button
|
|
@@ -4,7 +4,10 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
vi.mock("next/image", () => ({
|
|
7
|
-
default: (props: ComponentPropsWithoutRef<"img">) =>
|
|
7
|
+
default: ({ alt = "", ...props }: ComponentPropsWithoutRef<"img">) => (
|
|
8
|
+
// eslint-disable-next-line @next/next/no-img-element -- this is the next/image test double.
|
|
9
|
+
<img alt={alt} {...props} />
|
|
10
|
+
),
|
|
8
11
|
}));
|
|
9
12
|
|
|
10
13
|
import { AppSwitcher, type SuiteAppEntry } from "./app-switcher";
|
|
@@ -28,10 +31,10 @@ const APPS: SuiteAppEntry[] = [
|
|
|
28
31
|
icon: "/kraken.svg",
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
|
-
slug: "
|
|
32
|
-
name: "
|
|
34
|
+
slug: "shelly",
|
|
35
|
+
name: "Shelly",
|
|
33
36
|
description: "AI teammates",
|
|
34
|
-
icon: "/
|
|
37
|
+
icon: "/shelly.svg",
|
|
35
38
|
},
|
|
36
39
|
];
|
|
37
40
|
|
|
@@ -56,7 +59,11 @@ function TestDropdownItem({
|
|
|
56
59
|
children,
|
|
57
60
|
...props
|
|
58
61
|
}: ComponentPropsWithoutRef<"button">) {
|
|
59
|
-
return
|
|
62
|
+
return (
|
|
63
|
+
<button data-dropdown-item="true" {...props}>
|
|
64
|
+
{children}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
describe("AppSwitcher", () => {
|
|
@@ -85,12 +92,20 @@ describe("AppSwitcher", () => {
|
|
|
85
92
|
const newTabButton = screen.getByRole("button", {
|
|
86
93
|
name: "Open Kraken in a new tab",
|
|
87
94
|
});
|
|
88
|
-
expect(newTabButton).toHaveClass("absolute", "h-8"
|
|
95
|
+
expect(newTabButton).toHaveClass("absolute", "h-8");
|
|
96
|
+
expect(newTabButton).toHaveStyle({ width: "2rem" });
|
|
97
|
+
expect(newTabButton).toHaveAttribute("data-dropdown-item", "true");
|
|
89
98
|
expect(
|
|
90
99
|
screen.queryByRole("button", { name: "Open Tides in a new tab" }),
|
|
91
100
|
).not.toBeInTheDocument();
|
|
92
101
|
|
|
93
102
|
await user.click(newTabButton);
|
|
94
103
|
expect(onSelect).toHaveBeenCalledWith(APPS[2], { newTab: true });
|
|
104
|
+
|
|
105
|
+
onSelect.mockClear();
|
|
106
|
+
await user.click(
|
|
107
|
+
document.querySelector("[data-test='switch-app-kraken']") as HTMLElement,
|
|
108
|
+
);
|
|
109
|
+
expect(onSelect).toHaveBeenCalledWith(APPS[2]);
|
|
95
110
|
});
|
|
96
111
|
});
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import Image from "next/image";
|
|
4
4
|
import { useState, type ReactNode } from "react";
|
|
5
|
-
import {
|
|
6
|
-
NavArrowDown as ChevronDown,
|
|
7
|
-
OpenNewWindow,
|
|
8
|
-
} from "iconoir-react";
|
|
5
|
+
import { NavArrowDown as ChevronDown, OpenNewWindow } from "iconoir-react";
|
|
9
6
|
|
|
10
7
|
import { cn } from "../lib/cn";
|
|
11
8
|
import type {
|
|
@@ -24,11 +21,11 @@ export function appSwitcherTriggerClass(open: boolean, collapsed = false) {
|
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
export function appSwitcherMarkClass() {
|
|
27
|
-
return "h-8 w-8 shrink-0 rounded-lg
|
|
24
|
+
return "h-8 w-8 shrink-0 rounded-lg";
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export function appSwitcherMenuItemClass() {
|
|
31
|
-
return "rounded-lg px-2 py-2 data-[highlighted]:bg-transparent hover:bg-ph-muted/80";
|
|
28
|
+
return "rounded-lg px-2 py-2 data-[highlighted]:bg-transparent hover:bg-ph-muted/80 focus-visible:shadow-none";
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
export function AppSwitcherMark({
|
|
@@ -152,7 +149,6 @@ export function AppSwitcher({
|
|
|
152
149
|
<span
|
|
153
150
|
className={appSwitcherTriggerClass(open, collapsed)}
|
|
154
151
|
data-test="app-switcher-trigger"
|
|
155
|
-
aria-expanded={open}
|
|
156
152
|
>
|
|
157
153
|
<AppSwitcherMark collapsed={collapsed}>
|
|
158
154
|
{mark}
|
|
@@ -199,8 +195,7 @@ export function AppSwitcher({
|
|
|
199
195
|
</span>
|
|
200
196
|
</span>
|
|
201
197
|
</DropdownItem>
|
|
202
|
-
<
|
|
203
|
-
type="button"
|
|
198
|
+
<DropdownItem
|
|
204
199
|
id={`switch-app-${app.slug}-new-tab`}
|
|
205
200
|
data-test={`switch-app-${app.slug}-new-tab`}
|
|
206
201
|
aria-label={`Open ${app.name} in a new tab`}
|
|
@@ -209,10 +204,11 @@ export function AppSwitcher({
|
|
|
209
204
|
setOpen(false);
|
|
210
205
|
onSelect(app, { newTab: true });
|
|
211
206
|
}}
|
|
212
|
-
className="absolute right-1.5 top-1/2 z-[1] flex h-8
|
|
207
|
+
className="absolute right-1.5 top-1/2 z-[1] flex h-8 -translate-y-1/2 justify-center rounded-md p-0 text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:shadow-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ph-brand/35"
|
|
208
|
+
style={{ width: "2rem" }}
|
|
213
209
|
>
|
|
214
210
|
<OpenNewWindow className="h-4 w-4" aria-hidden="true" />
|
|
215
|
-
</
|
|
211
|
+
</DropdownItem>
|
|
216
212
|
</div>
|
|
217
213
|
))}
|
|
218
214
|
</div>
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { type ReactNode, type PointerEvent } from "react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
|
+
import {
|
|
7
|
+
SIDEBAR_MAX_WIDTH,
|
|
8
|
+
SIDEBAR_RAIL_WIDTH,
|
|
9
|
+
} from "../lib/use-sidebar-width";
|
|
6
10
|
|
|
7
11
|
export interface SuiteAppLayoutProps {
|
|
8
12
|
sidebar: ReactNode;
|
|
@@ -54,6 +58,7 @@ export function SuiteAppLayout({
|
|
|
54
58
|
)}
|
|
55
59
|
>
|
|
56
60
|
<aside
|
|
61
|
+
aria-label="Sidebar"
|
|
57
62
|
className="relative hidden shrink-0 flex-col overflow-visible border-r border-ph-border ease-out lg:flex"
|
|
58
63
|
style={{ width: `${sidebarWidth}px` }}
|
|
59
64
|
data-collapsed={collapsed ? "true" : "false"}
|
|
@@ -65,6 +70,10 @@ export function SuiteAppLayout({
|
|
|
65
70
|
role="separator"
|
|
66
71
|
aria-orientation="vertical"
|
|
67
72
|
aria-label="Resize sidebar"
|
|
73
|
+
aria-valuenow={sidebarWidth}
|
|
74
|
+
aria-valuemin={SIDEBAR_RAIL_WIDTH}
|
|
75
|
+
aria-valuemax={SIDEBAR_MAX_WIDTH}
|
|
76
|
+
aria-valuetext={`${sidebarWidth} pixels`}
|
|
68
77
|
data-test="sidebar-resize-handle"
|
|
69
78
|
tabIndex={onResizeBy ? 0 : undefined}
|
|
70
79
|
onPointerDown={onStartResize}
|
|
@@ -81,7 +90,7 @@ export function SuiteAppLayout({
|
|
|
81
90
|
onResizeBy?.(16);
|
|
82
91
|
}
|
|
83
92
|
}}
|
|
84
|
-
className="absolute inset-y-0 right-0 z-20 w-2 cursor-col-resize touch-none transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
|
|
93
|
+
className="absolute inset-y-0 right-0 z-20 w-2 cursor-col-resize touch-none transition-colors before:absolute before:-left-2 before:-right-2 before:inset-y-0 before:content-[''] hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
|
|
85
94
|
/>
|
|
86
95
|
) : null}
|
|
87
96
|
</aside>
|
|
@@ -92,10 +101,10 @@ export function SuiteAppLayout({
|
|
|
92
101
|
) : null}
|
|
93
102
|
<main
|
|
94
103
|
id="main-content"
|
|
95
|
-
tabIndex={
|
|
104
|
+
tabIndex={0}
|
|
96
105
|
data-lock-scroll={lockMainScroll ? "true" : undefined}
|
|
97
106
|
className={cn(
|
|
98
|
-
"flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none",
|
|
107
|
+
"flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ph-focus",
|
|
99
108
|
"suite-app-layout__main",
|
|
100
109
|
lockMainScroll ? "overflow-hidden" : "overflow-y-auto",
|
|
101
110
|
)}
|
|
@@ -40,7 +40,6 @@ export function SuiteBottomNav({
|
|
|
40
40
|
className?: string;
|
|
41
41
|
}) {
|
|
42
42
|
if (process.env.NODE_ENV !== "production" && items.length > 5) {
|
|
43
|
-
// eslint-disable-next-line no-console
|
|
44
43
|
console.warn(
|
|
45
44
|
`SuiteBottomNav: ${items.length} items — bottom nav should have at most 5 (Hick's Law).`,
|
|
46
45
|
);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { SuiteClientsProjectsDirectory } from "@/suite/components/suite-clients-projects-directory";
|
|
5
|
+
|
|
6
|
+
describe("SuiteClientsProjectsDirectory", () => {
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
it("uses roving focus and arrow keys for the icon radiogroup", async () => {
|
|
10
|
+
const user = userEvent.setup();
|
|
11
|
+
|
|
12
|
+
render(
|
|
13
|
+
<SuiteClientsProjectsDirectory
|
|
14
|
+
clients={[]}
|
|
15
|
+
projects={[]}
|
|
16
|
+
canEdit
|
|
17
|
+
onCreateClient={vi.fn()}
|
|
18
|
+
/>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await user.click(screen.getByRole("button", { name: "New client" }));
|
|
22
|
+
const castle = screen.getByRole("radio", { name: "Castle" });
|
|
23
|
+
const castleFlag = screen.getByRole("radio", { name: "Castle flag" });
|
|
24
|
+
|
|
25
|
+
expect(castle).toHaveAttribute("tabindex", "0");
|
|
26
|
+
expect(castleFlag).toHaveAttribute("tabindex", "-1");
|
|
27
|
+
|
|
28
|
+
castle.focus();
|
|
29
|
+
await user.keyboard("{ArrowRight}");
|
|
30
|
+
expect(castleFlag).toHaveAttribute("aria-checked", "true");
|
|
31
|
+
expect(castleFlag).toHaveFocus();
|
|
32
|
+
|
|
33
|
+
await user.keyboard("{Home}");
|
|
34
|
+
expect(screen.getByRole("radio", { name: "Arena" })).toHaveFocus();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("moves focus between the mobile client list and detail view", async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
const originalMatchMedia = window.matchMedia;
|
|
40
|
+
Object.defineProperty(window, "matchMedia", {
|
|
41
|
+
configurable: true,
|
|
42
|
+
value: vi.fn().mockReturnValue({ matches: true }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
render(
|
|
47
|
+
<SuiteClientsProjectsDirectory
|
|
48
|
+
clients={[{ id: "acme", name: "Acme" }]}
|
|
49
|
+
projects={[]}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await user.click(screen.getByRole("option", { name: /Acme/ }));
|
|
54
|
+
await waitFor(() =>
|
|
55
|
+
expect(
|
|
56
|
+
screen.getByRole("button", { name: "Back to clients" }),
|
|
57
|
+
).toHaveFocus(),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await user.click(screen.getByRole("button", { name: "Back to clients" }));
|
|
61
|
+
await waitFor(() =>
|
|
62
|
+
expect(screen.getByRole("option", { name: /Acme/ })).toHaveFocus(),
|
|
63
|
+
);
|
|
64
|
+
} finally {
|
|
65
|
+
Object.defineProperty(window, "matchMedia", {
|
|
66
|
+
configurable: true,
|
|
67
|
+
value: originalMatchMedia,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|