@xenide-io/the-old-ui-theme 0.5.3 → 0.5.4

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.
Files changed (49) hide show
  1. package/dist/{index-Od4pIP4F.d.mts → Chip-C5hRFAhe.d.mts} +10 -606
  2. package/dist/{index-Od4pIP4F.d.ts → Chip-C5hRFAhe.d.ts} +10 -606
  3. package/dist/{chunk-G53YLHVE.mjs → chunk-E5ZBQYEX.mjs} +59 -9
  4. package/dist/{chunk-VFTZXPXM.mjs → chunk-SBBKXV35.mjs} +144 -79
  5. package/dist/index.d.mts +3 -79
  6. package/dist/index.d.ts +3 -79
  7. package/dist/index.js +524 -3595
  8. package/dist/index.mjs +3 -765
  9. package/dist/suite.d.mts +38 -12
  10. package/dist/suite.d.ts +38 -12
  11. package/dist/suite.js +84 -15
  12. package/dist/suite.mjs +53 -13
  13. package/dist/ui.d.mts +608 -2
  14. package/dist/ui.d.ts +608 -2
  15. package/dist/ui.js +202 -87
  16. package/dist/ui.mjs +2 -2
  17. package/package.json +2 -6
  18. package/scripts/install-theme-dev.sh +60 -0
  19. package/scripts/sync-posthog-icons.mjs +75 -0
  20. package/src/components/ui/Chip.tsx +36 -18
  21. package/src/components/ui/CommandPalette.test.tsx +41 -0
  22. package/src/components/ui/CommandPalette.tsx +58 -9
  23. package/src/components/ui/DropdownMenu.tsx +50 -11
  24. package/src/components/ui/SegmentedControl.tsx +2 -1
  25. package/src/components/ui/SettingsLayout.tsx +19 -5
  26. package/src/components/ui/interaction-primitives.test.tsx +52 -0
  27. package/src/styles/suite-skin.css +17 -35
  28. package/src/suite/components/app-switcher.tsx +32 -25
  29. package/src/suite/components/command-palette-host.tsx +20 -8
  30. package/src/suite/components/suite-app-layout.tsx +1 -1
  31. package/src/suite/components/suite-bottom-nav.tsx +23 -18
  32. package/src/suite/components/suite-layout.test.tsx +44 -0
  33. package/src/suite/components/suite-layout.tsx +3 -3
  34. package/src/suite/components/suite-mobile-drawer.tsx +12 -0
  35. package/src/suite/components/suite-mobile-header.tsx +15 -9
  36. package/src/suite/components/suite-notification-bell.tsx +23 -11
  37. package/src/suite/components/suite-settings-mobile-nav.tsx +2 -0
  38. package/src/suite/components/suite-sidebar.tsx +6 -3
  39. package/src/suite/components/suite-user-menu.tsx +6 -0
  40. package/src/suite/lib/injected.ts +12 -11
  41. package/src/components/charts/index.ts +0 -9
  42. package/src/components/charts/quill/BarChart.tsx +0 -85
  43. package/src/components/charts/quill/FunnelChart.tsx +0 -49
  44. package/src/components/charts/quill/InsightShell.tsx +0 -27
  45. package/src/components/charts/quill/MetricCard.tsx +0 -44
  46. package/src/components/charts/quill/PieChart.tsx +0 -81
  47. package/src/components/charts/quill/Sparkline.tsx +0 -57
  48. package/src/components/charts/quill/TimeSeriesLineChart.tsx +0 -62
  49. package/src/components/charts/quill/index.ts +0 -9
@@ -1,14 +1,14 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState } from "react";
4
4
 
5
5
  import type {
6
6
  SuiteCommandItem,
7
7
  SuiteCommandPaletteComponent,
8
- } from '../lib/injected';
8
+ } from "../lib/injected";
9
9
 
10
10
  function isEditableTarget(): boolean {
11
- if (typeof document === 'undefined') return false;
11
+ if (typeof document === "undefined") return false;
12
12
  const el = document.activeElement;
13
13
  return (
14
14
  el instanceof HTMLElement &&
@@ -27,26 +27,34 @@ function isEditableTarget(): boolean {
27
27
  export function CommandPaletteHost({
28
28
  items,
29
29
  isAuthenticated = true,
30
- placeholder = 'Type a command or search…',
30
+ placeholder = "Type a command or search…",
31
+ id,
32
+ dataTest,
33
+ inputId,
34
+ inputDataTest,
31
35
  commandPalette: CommandPalette,
32
36
  }: {
33
37
  items: SuiteCommandItem[];
34
38
  isAuthenticated?: boolean;
35
39
  placeholder?: string;
40
+ id?: string;
41
+ dataTest?: string;
42
+ inputId?: string;
43
+ inputDataTest?: string;
36
44
  commandPalette: SuiteCommandPaletteComponent;
37
45
  }) {
38
46
  const [open, setOpen] = useState(false);
39
47
 
40
48
  useEffect(() => {
41
49
  const onKey = (e: KeyboardEvent) => {
42
- if (!((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) return;
50
+ if (!((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k")) return;
43
51
  // Leave editors (BlockNote uses Cmd/Ctrl+K for links) alone unless the palette is open.
44
52
  if (!open && isEditableTarget()) return;
45
53
  e.preventDefault();
46
54
  setOpen((o) => !o);
47
55
  };
48
- window.addEventListener('keydown', onKey);
49
- return () => window.removeEventListener('keydown', onKey);
56
+ window.addEventListener("keydown", onKey);
57
+ return () => window.removeEventListener("keydown", onKey);
50
58
  }, [open]);
51
59
 
52
60
  if (!isAuthenticated) return null;
@@ -57,6 +65,10 @@ export function CommandPaletteHost({
57
65
  isOpen={open}
58
66
  onClose={() => setOpen(false)}
59
67
  placeholder={placeholder}
68
+ id={id}
69
+ dataTest={dataTest}
70
+ inputId={inputId}
71
+ inputDataTest={inputDataTest}
60
72
  />
61
73
  );
62
74
  }
@@ -49,7 +49,7 @@ export function SuiteAppLayout({
49
49
  <div
50
50
  data-test="app-shell"
51
51
  className={cn(
52
- "suite-app-layout flex min-h-dvh lg:h-screen lg:overflow-hidden",
52
+ "suite-app-layout flex h-dvh overflow-hidden",
53
53
  className,
54
54
  )}
55
55
  >
@@ -1,15 +1,15 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import Link from 'next/link';
4
- import type { ComponentType } from 'react';
3
+ import Link from "next/link";
4
+ import type { ComponentType } from "react";
5
5
 
6
- import { cn } from '../lib/cn';
6
+ import { cn } from "../lib/cn";
7
7
 
8
8
  /** Any icon component that accepts sizing/stroke props (Lucide, theme icons). */
9
9
  export type SuiteNavIcon = ComponentType<{
10
10
  className?: string;
11
11
  strokeWidth?: number | string;
12
- 'aria-hidden'?: boolean | 'true' | 'false';
12
+ "aria-hidden"?: boolean | "true" | "false";
13
13
  }>;
14
14
 
15
15
  /**
@@ -23,14 +23,15 @@ export interface SuiteBottomNavItem {
23
23
  href: string;
24
24
  label: string;
25
25
  icon: SuiteNavIcon;
26
+ id?: string;
26
27
  active?: boolean;
27
28
  dataTest?: string;
28
29
  }
29
30
 
30
31
  export function SuiteBottomNav({
31
32
  items,
32
- ariaLabel = 'Primary navigation',
33
- dataTest = 'suite-bottom-nav',
33
+ ariaLabel = "Primary navigation",
34
+ dataTest = "suite-bottom-nav",
34
35
  className,
35
36
  }: {
36
37
  items: readonly SuiteBottomNavItem[];
@@ -38,7 +39,7 @@ export function SuiteBottomNav({
38
39
  dataTest?: string;
39
40
  className?: string;
40
41
  }) {
41
- if (process.env.NODE_ENV !== 'production' && items.length > 5) {
42
+ if (process.env.NODE_ENV !== "production" && items.length > 5) {
42
43
  // eslint-disable-next-line no-console
43
44
  console.warn(
44
45
  `SuiteBottomNav: ${items.length} items — bottom nav should have at most 5 (Hick's Law).`,
@@ -54,20 +55,20 @@ export function SuiteBottomNav({
54
55
  <div
55
56
  aria-hidden
56
57
  className="shrink-0 lg:hidden"
57
- style={{ height: 'calc(60px + env(safe-area-inset-bottom))' }}
58
+ style={{ height: "calc(60px + env(safe-area-inset-bottom))" }}
58
59
  />
59
60
  <nav
60
61
  aria-label={ariaLabel}
61
62
  data-test={dataTest}
62
63
  className={cn(
63
- 'suite-bottom-nav no-print z-40 grid auto-cols-fr grid-flow-col lg:hidden',
64
+ "suite-bottom-nav no-print z-40 grid auto-cols-fr grid-flow-col lg:hidden",
64
65
  className,
65
66
  )}
66
67
  style={
67
68
  {
68
69
  gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))`,
69
- '--suite-nav-items': items.length,
70
- '--suite-nav-active': activeIndex >= 0 ? activeIndex : 0,
70
+ "--suite-nav-items": items.length,
71
+ "--suite-nav-active": activeIndex >= 0 ? activeIndex : 0,
71
72
  } as React.CSSProperties
72
73
  }
73
74
  >
@@ -82,17 +83,21 @@ export function SuiteBottomNav({
82
83
  <Link
83
84
  key={item.href}
84
85
  href={item.href}
85
- data-test={item.dataTest ?? `suite-nav-${item.label.toLowerCase().replace(/\s+/g, '-')}`}
86
- aria-current={active ? 'page' : undefined}
86
+ id={item.id}
87
+ data-test={
88
+ item.dataTest ??
89
+ `suite-nav-${item.label.toLowerCase().replace(/\s+/g, "-")}`
90
+ }
91
+ aria-current={active ? "page" : undefined}
87
92
  className={cn(
88
- 'suite-bottom-nav__item relative flex min-h-[60px] min-w-0 flex-col items-center justify-center gap-1 px-1 pb-1.5 pt-2 text-[10px] font-medium leading-none text-ph-mutedtext transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ph-brand motion-reduce:transition-none',
89
- active && 'font-semibold text-ph-brand',
93
+ "suite-bottom-nav__item relative flex min-h-[60px] min-w-0 flex-col items-center justify-center gap-1 px-1 pb-1.5 pt-2 text-[10px] font-medium leading-none text-ph-mutedtext transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ph-brand motion-reduce:transition-none",
94
+ active && "font-semibold text-ph-brand",
90
95
  )}
91
96
  >
92
97
  <span
93
98
  className={cn(
94
- 'suite-bottom-nav__icon relative z-10 inline-flex items-center justify-center',
95
- active && 'suite-bottom-nav__icon--active',
99
+ "suite-bottom-nav__icon relative z-10 inline-flex items-center justify-center",
100
+ active && "suite-bottom-nav__icon--active",
96
101
  )}
97
102
  >
98
103
  <Icon
@@ -0,0 +1,44 @@
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { SuiteAppLayout } from "./suite-app-layout";
5
+ import { SuitePageHeader } from "./suite-layout";
6
+
7
+ describe("suite scroll ownership", () => {
8
+ it("keeps mobile chrome outside the main scrollport and locks only on request", () => {
9
+ render(
10
+ <SuiteAppLayout
11
+ sidebar={<div>Sidebar</div>}
12
+ mobileHeader={<header>Mobile chrome</header>}
13
+ lockMainScroll
14
+ >
15
+ <div>Page content</div>
16
+ </SuiteAppLayout>,
17
+ );
18
+
19
+ const main = screen.getByRole("main");
20
+ const mobileHeader = screen.getByText("Mobile chrome").closest("header");
21
+
22
+ expect(main).toHaveAttribute("data-lock-scroll", "true");
23
+ expect(main).toHaveClass("overflow-hidden");
24
+ expect(main).not.toContainElement(mobileHeader);
25
+ expect(within(main).getByText("Page content")).toBeInTheDocument();
26
+ });
27
+
28
+ it("lets page headers and actions reflow without clipping narrow screens", () => {
29
+ render(
30
+ <SuitePageHeader
31
+ title="A very long responsive page title"
32
+ actions={<button type="button">Export report</button>}
33
+ />,
34
+ );
35
+
36
+ expect(
37
+ screen.getByRole("heading", {
38
+ name: "A very long responsive page title",
39
+ }),
40
+ ).toHaveClass("break-words", "text-balance");
41
+ expect(screen.getByRole("button", { name: "Export report" }).parentElement)
42
+ .toHaveClass("max-w-full", "overflow-x-auto");
43
+ });
44
+ });
@@ -181,7 +181,7 @@ export function SuitePageHeader({
181
181
  data-test={dataTest}
182
182
  data-stuck={sticky ? (stuck ? 'true' : 'false') : undefined}
183
183
  className={cn(
184
- 'flex items-start justify-between gap-4 pb-2 sm:pb-3',
184
+ 'flex min-w-0 flex-col items-start gap-3 pb-2 sm:flex-row sm:justify-between sm:gap-4 sm:pb-3',
185
185
  // No extra pt here — page inset sets the shared title baseline.
186
186
  // Stuck state adds pad via .suite-page-header-sticky[data-stuck].
187
187
  sticky && 'suite-page-header-sticky',
@@ -212,7 +212,7 @@ export function SuitePageHeader({
212
212
  <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
213
213
  <h1
214
214
  className={cn(
215
- 'font-display text-[1.625rem] font-bold leading-none tracking-tight text-ph-ink sm:text-[2.25rem]',
215
+ 'min-w-0 break-words font-display text-[1.625rem] font-bold leading-none tracking-tight text-ph-ink text-balance sm:text-[2.25rem]',
216
216
  titleClassName,
217
217
  )}
218
218
  >
@@ -237,7 +237,7 @@ export function SuitePageHeader({
237
237
  ) : null}
238
238
  </div>
239
239
  {actions ? (
240
- <div className="flex shrink-0 items-center gap-2 self-center">
240
+ <div className="flex max-w-full items-center gap-2 self-stretch overflow-x-auto pb-1 sm:shrink-0 sm:self-center sm:overflow-visible sm:pb-0">
241
241
  {actions}
242
242
  </div>
243
243
  ) : null}
@@ -23,6 +23,10 @@ export function SuiteMobileDrawer({
23
23
  durationMs = 200,
24
24
  dataTest,
25
25
  panelDataTest,
26
+ backdropId,
27
+ backdropDataTest,
28
+ closeButtonId,
29
+ closeButtonDataTest,
26
30
  panelClassName,
27
31
  }: {
28
32
  open: boolean;
@@ -42,6 +46,10 @@ export function SuiteMobileDrawer({
42
46
  durationMs?: number;
43
47
  dataTest?: string;
44
48
  panelDataTest?: string;
49
+ backdropId?: string;
50
+ backdropDataTest?: string;
51
+ closeButtonId?: string;
52
+ closeButtonDataTest?: string;
45
53
  /** Panel surface overrides (e.g. `bg-ph-canvas`). */
46
54
  panelClassName?: string;
47
55
  }) {
@@ -106,6 +114,8 @@ export function SuiteMobileDrawer({
106
114
  <div data-test={dataTest} className="no-print fixed inset-0 z-50 lg:hidden">
107
115
  <button
108
116
  type="button"
117
+ id={backdropId}
118
+ data-test={backdropDataTest}
109
119
  tabIndex={-1}
110
120
  className={cn(
111
121
  "absolute inset-0 bg-black/25 backdrop-blur-sm motion-safe:transition-opacity motion-safe:ease-spring-subtle",
@@ -148,6 +158,8 @@ export function SuiteMobileDrawer({
148
158
  ) : null}
149
159
  <button
150
160
  type="button"
161
+ id={closeButtonId}
162
+ data-test={closeButtonDataTest}
151
163
  onClick={onClose}
152
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"
153
165
  aria-label={closeLabel}
@@ -1,11 +1,14 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import type { ComponentType, ReactNode } from 'react';
4
- import { Menu } from 'iconoir-react';
3
+ import type { ComponentType, ReactNode } from "react";
4
+ import { Menu } from "iconoir-react";
5
5
 
6
- import { cn } from '../lib/cn';
6
+ import { cn } from "../lib/cn";
7
7
 
8
- type SuiteMenuIcon = ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>;
8
+ type SuiteMenuIcon = ComponentType<{
9
+ className?: string;
10
+ "aria-hidden"?: boolean | "true" | "false";
11
+ }>;
9
12
 
10
13
  /**
11
14
  * Suite mobile app bar — the standardized top of every app on phone/tablet.
@@ -18,7 +21,8 @@ export function SuiteMobileHeader({
18
21
  menuIcon: MenuIcon = Menu,
19
22
  title,
20
23
  actions,
21
- menuLabel = 'Open navigation',
24
+ menuLabel = "Open navigation",
25
+ menuId,
22
26
  menuDataTest,
23
27
  dataTest,
24
28
  className,
@@ -32,6 +36,7 @@ export function SuiteMobileHeader({
32
36
  /** Right-hand slot — search, bell, avatar (keep this order). */
33
37
  actions?: ReactNode;
34
38
  menuLabel?: string;
39
+ menuId?: string;
35
40
  menuDataTest?: string;
36
41
  dataTest?: string;
37
42
  className?: string;
@@ -40,13 +45,14 @@ export function SuiteMobileHeader({
40
45
  <header
41
46
  data-test={dataTest}
42
47
  className={cn(
43
- 'suite-app-bar no-print sticky top-0 z-30 flex min-h-[56px] shrink-0 items-center gap-1 border-b border-ph-border px-2 lg:hidden',
48
+ "suite-app-bar no-print sticky top-0 z-30 flex min-h-[56px] shrink-0 items-center gap-1 border-b border-ph-border px-2 lg:hidden",
44
49
  className,
45
50
  )}
46
51
  >
47
52
  {onMenuClick ? (
48
53
  <button
49
54
  type="button"
55
+ id={menuId}
50
56
  data-test={menuDataTest}
51
57
  className="suite-press inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
52
58
  onClick={onMenuClick}
@@ -57,8 +63,8 @@ export function SuiteMobileHeader({
57
63
  ) : null}
58
64
  <div
59
65
  className={cn(
60
- 'flex min-w-0 flex-1 items-center',
61
- !onMenuClick && 'pl-2',
66
+ "flex min-w-0 flex-1 items-center",
67
+ !onMenuClick && "pl-2",
62
68
  )}
63
69
  >
64
70
  {title}
@@ -1,11 +1,11 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useCallback, useEffect, useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import { Bell } from 'iconoir-react';
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Bell } from "iconoir-react";
6
6
 
7
- import type { SuiteDropdownMenuComponent } from '../lib/injected';
8
- import { resolveSuiteNotificationHref } from '../lib/apps';
7
+ import type { SuiteDropdownMenuComponent } from "../lib/injected";
8
+ import { resolveSuiteNotificationHref } from "../lib/apps";
9
9
 
10
10
  const POLL_MS = 60_000;
11
11
 
@@ -30,9 +30,9 @@ export interface SuiteNotificationsResponse {
30
30
 
31
31
  function timeAgo(iso: string): string {
32
32
  const then = new Date(iso).getTime();
33
- if (Number.isNaN(then)) return '';
33
+ if (Number.isNaN(then)) return "";
34
34
  const secs = Math.max(0, Math.round((Date.now() - then) / 1000));
35
- if (secs < 60) return 'just now';
35
+ if (secs < 60) return "just now";
36
36
  const mins = Math.round(secs / 60);
37
37
  if (mins < 60) return `${mins}m ago`;
38
38
  const hrs = Math.round(mins / 60);
@@ -45,7 +45,10 @@ export function SuiteNotificationBell({
45
45
  markRead,
46
46
  markAllRead,
47
47
  dropdownMenu: DropdownMenu,
48
- cacheKey = 'suite-notifications-cache',
48
+ cacheKey = "suite-notifications-cache",
49
+ dataTest = "suite-notifications",
50
+ triggerId,
51
+ triggerDataTest,
49
52
  }: {
50
53
  fetchNotifications: () => Promise<SuiteNotificationsResponse>;
51
54
  markRead: (id: string) => Promise<unknown>;
@@ -53,6 +56,9 @@ export function SuiteNotificationBell({
53
56
  dropdownMenu: SuiteDropdownMenuComponent;
54
57
  /** sessionStorage key for stale-while-revalidate; `null` disables caching. */
55
58
  cacheKey?: string | null;
59
+ dataTest?: string;
60
+ triggerId?: string;
61
+ triggerDataTest?: string;
56
62
  }) {
57
63
  const router = useRouter();
58
64
  const [open, setOpen] = useState(false);
@@ -165,16 +171,18 @@ export function SuiteNotificationBell({
165
171
  return (
166
172
  <DropdownMenu
167
173
  aria-label="Notifications"
174
+ triggerId={triggerId ?? `${dataTest}-trigger`}
175
+ triggerDataTest={triggerDataTest ?? `${dataTest}-trigger`}
168
176
  align="end"
169
177
  panelClassName="w-80 overflow-hidden p-0"
170
178
  open={open}
171
179
  onOpenChange={onOpenChange}
172
180
  trigger={
173
- <span className="relative inline-flex h-9 w-9 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink">
181
+ <span className="relative inline-flex h-11 w-11 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink">
174
182
  <Bell className="h-5 w-5" strokeWidth={1.75} aria-hidden />
175
183
  {unread > 0 ? (
176
184
  <span className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-ph-brand px-1 text-[10px] font-semibold leading-none text-white ring-2 ring-ph-surface">
177
- {unread > 9 ? '9+' : unread}
185
+ {unread > 9 ? "9+" : unread}
178
186
  </span>
179
187
  ) : null}
180
188
  </span>
@@ -185,6 +193,8 @@ export function SuiteNotificationBell({
185
193
  {unread > 0 ? (
186
194
  <button
187
195
  type="button"
196
+ id={`${dataTest}-mark-all`}
197
+ data-test={`${dataTest}-mark-all`}
188
198
  onClick={markAll}
189
199
  className="text-xs font-medium text-ph-brand hover:underline"
190
200
  >
@@ -202,6 +212,8 @@ export function SuiteNotificationBell({
202
212
  <button
203
213
  key={n.id}
204
214
  type="button"
215
+ id={`${dataTest}-item-${n.id}`}
216
+ data-test={`${dataTest}-item-${n.id}`}
205
217
  onClick={() => void openItem(n)}
206
218
  className="flex w-full flex-col gap-0.5 px-3 py-2 text-left transition hover:bg-ph-muted"
207
219
  >
@@ -7,6 +7,7 @@ export interface SuiteSettingsNavItem {
7
7
  label: string;
8
8
  href: string;
9
9
  icon: ElementType | ReactNode;
10
+ id?: string;
10
11
  testId?: string;
11
12
  }
12
13
 
@@ -59,6 +60,7 @@ export function SuiteSettingsMobileNav({
59
60
  <button
60
61
  key={item.href}
61
62
  type="button"
63
+ id={item.id}
62
64
  data-test={item.testId}
63
65
  onClick={() => onSelect(item.href)}
64
66
  aria-current={active ? "page" : undefined}
@@ -13,6 +13,8 @@ export interface SuiteSidebarNavItem {
13
13
  href: string;
14
14
  label: string;
15
15
  icon: SuiteNavIcon;
16
+ id?: string;
17
+ dataTest?: string;
16
18
  active?: boolean;
17
19
  onClick?: () => void;
18
20
  badge?: ReactNode;
@@ -115,16 +117,17 @@ export function SuiteSidebar({
115
117
  <Link
116
118
  key={item.label}
117
119
  href={item.href}
118
- data-test="nav-link"
120
+ id={item.id}
121
+ data-test={item.dataTest ?? "nav-link"}
119
122
  aria-current={active ? "page" : undefined}
120
123
  onClick={item.onClick}
121
124
  className={cn(
122
- "group relative flex items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors",
125
+ "group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors",
123
126
  active
124
127
  ? "bg-ph-muted"
125
128
  : "text-ph-subtle hover:bg-ph-muted hover:text-ph-ink",
126
129
  collapsed
127
- ? "mx-auto size-10 shrink-0 justify-center gap-0 px-0 py-0"
130
+ ? "mx-auto size-11 shrink-0 justify-center gap-0 px-0 py-0"
128
131
  : "w-full px-2.5 py-2",
129
132
  )}
130
133
  >
@@ -19,6 +19,8 @@ export interface SuiteUserMenuProps {
19
19
  /** Show the old desktop sidebar sign-out action beside the avatar. */
20
20
  showSignOutAction?: boolean;
21
21
  dataTest?: string;
22
+ triggerId?: string;
23
+ triggerDataTest?: string;
22
24
  className?: string;
23
25
  }
24
26
 
@@ -50,6 +52,8 @@ export function SuiteUserMenu({
50
52
  fallbackInitials,
51
53
  showSignOutAction = false,
52
54
  dataTest = "suite-user-menu",
55
+ triggerId,
56
+ triggerDataTest,
53
57
  className,
54
58
  }: SuiteUserMenuProps) {
55
59
  const router = useRouter();
@@ -77,6 +81,8 @@ export function SuiteUserMenu({
77
81
  const accountMenu = (
78
82
  <DropdownMenu
79
83
  trigger={avatar}
84
+ triggerId={triggerId ?? `${dataTest}-trigger`}
85
+ triggerDataTest={triggerDataTest ?? `${dataTest}-trigger`}
80
86
  aria-label="Account menu"
81
87
  align="end"
82
88
  side="bottom"
@@ -1,8 +1,4 @@
1
- import type {
2
- ButtonHTMLAttributes,
3
- ComponentType,
4
- ReactNode,
5
- } from 'react';
1
+ import type { ButtonHTMLAttributes, ComponentType, ReactNode } from "react";
6
2
 
7
3
  /**
8
4
  * Structural types for theme primitives each app injects from
@@ -12,10 +8,12 @@ import type {
12
8
 
13
9
  export interface SuiteDropdownMenuProps {
14
10
  trigger: ReactNode;
11
+ triggerId?: string;
12
+ triggerDataTest?: string;
15
13
  children: ReactNode;
16
- 'aria-label': string;
17
- align?: 'start' | 'center' | 'end';
18
- side?: 'top' | 'right' | 'bottom' | 'left';
14
+ "aria-label": string;
15
+ align?: "start" | "center" | "end";
16
+ side?: "top" | "right" | "bottom" | "left";
19
17
  sideOffset?: number;
20
18
  className?: string;
21
19
  panelClassName?: string;
@@ -23,8 +21,7 @@ export interface SuiteDropdownMenuProps {
23
21
  onOpenChange?: (open: boolean) => void;
24
22
  }
25
23
 
26
- export type SuiteDropdownMenuComponent =
27
- ComponentType<SuiteDropdownMenuProps>;
24
+ export type SuiteDropdownMenuComponent = ComponentType<SuiteDropdownMenuProps>;
28
25
 
29
26
  export type SuiteDropdownItemComponent = ComponentType<
30
27
  ButtonHTMLAttributes<HTMLButtonElement>
@@ -44,11 +41,15 @@ export type SuiteCommandPaletteComponent = ComponentType<{
44
41
  onClose: () => void;
45
42
  placeholder?: string;
46
43
  className?: string;
44
+ id?: string;
45
+ dataTest?: string;
46
+ inputId?: string;
47
+ inputDataTest?: string;
47
48
  }>;
48
49
 
49
50
  /** App-branded spinner (each app has its own AppSpinner design). */
50
51
  export type SuiteSpinnerComponent = ComponentType<{
51
- size?: 'sm' | 'md' | 'lg';
52
+ size?: "sm" | "md" | "lg";
52
53
  className?: string;
53
54
  label?: string;
54
55
  }>;
@@ -1,9 +0,0 @@
1
- "use client";
2
-
3
- export { TimeSeriesLineChart, BarChart, PieChart, Sparkline, MetricCard, FunnelChart } from "./quill";
4
- export type { TimeSeriesLineChartProps, TimeSeriesLineChartSeries } from "./quill";
5
- export type { BarChartProps, BarChartSeries } from "./quill";
6
- export type { PieChartProps, PieChartSlice } from "./quill";
7
- export type { SparklineProps } from "./quill";
8
- export type { MetricCardProps } from "./quill";
9
- export type { FunnelChartProps, FunnelStep } from "./quill/FunnelChart";
@@ -1,85 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo } from "react";
4
- import { useChartTokens } from "@/lib/chart/use-ph-chart-tokens";
5
- import { cn } from "@/lib/cn";
6
-
7
- export interface BarChartSeries {
8
- key: string;
9
- label: string;
10
- data: number[];
11
- color?: string;
12
- }
13
-
14
- export interface BarChartProps {
15
- series: BarChartSeries[];
16
- labels: string[];
17
- className?: string;
18
- height?: number;
19
- }
20
-
21
- export function BarChart({ series, labels, className, height = 280 }: BarChartProps) {
22
- const tokens = useChartTokens();
23
- const groupCount = labels.length;
24
- const barCount = series.length;
25
-
26
- const { maxVal, chartData } = useMemo(() => {
27
- let mx = 0;
28
- const flat = series.flatMap((s) => s.data);
29
- for (const v of flat) if (v > mx) mx = v;
30
- return {
31
- maxVal: mx || 1,
32
- chartData: series.map((s, i) => ({
33
- label: s.label,
34
- color: s.color ?? tokens.series[i % tokens.series.length],
35
- values: s.data,
36
- })),
37
- };
38
- }, [series, tokens.series]);
39
-
40
- const padding = { top: 16, right: 8, bottom: 24, left: 8 };
41
- const innerW = 400 - padding.left - padding.right;
42
- const innerH = height - padding.top - padding.bottom;
43
- const groupW = innerW / groupCount;
44
- const barW = Math.max(4, (groupW * 0.7) / barCount);
45
- const gap = groupW * 0.15;
46
-
47
- return (
48
- <div className={cn("relative w-full", className)} style={{ height }}>
49
- <svg viewBox={`0 0 400 ${height}`} className="h-full w-full" preserveAspectRatio="xMidYMid meet" suppressHydrationWarning>
50
- {chartData.map((bar, bi) =>
51
- bar.values.map((v, li) => {
52
- const x = padding.left + li * groupW + gap / 2 + bi * barW;
53
- const barH = (v / maxVal) * (innerH - 4);
54
- const y = padding.top + innerH - barH;
55
- return (
56
- <rect
57
- key={`${bi}-${li}`}
58
- x={x}
59
- y={y}
60
- width={barW}
61
- height={barH}
62
- fill={bar.color}
63
- rx={2}
64
- >
65
- <title>{`${bar.label}: ${v}`}</title>
66
- </rect>
67
- );
68
- })
69
- )}
70
- {labels.map((l, i) => (
71
- <text
72
- key={l}
73
- x={padding.left + i * groupW + groupW / 2}
74
- y={height - 4}
75
- textAnchor="middle"
76
- className="fill-ph-mutedtext"
77
- fontSize="10"
78
- >
79
- {l}
80
- </text>
81
- ))}
82
- </svg>
83
- </div>
84
- );
85
- }