@xenide-io/the-old-ui-theme 0.10.0 → 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.
Files changed (42) hide show
  1. package/dist/{Chip-DKT-82Lf.d.mts → Chip-Dw2g2t8D.d.mts} +14 -2
  2. package/dist/{Chip-DKT-82Lf.d.ts → Chip-Dw2g2t8D.d.ts} +14 -2
  3. package/dist/{chunk-LHE4ROON.mjs → chunk-RIPGN6FG.mjs} +412 -61
  4. package/dist/index.d.mts +3 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +355 -23
  7. package/dist/index.mjs +11 -3
  8. package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
  9. package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
  10. package/dist/suite.d.mts +14 -4
  11. package/dist/suite.d.ts +14 -4
  12. package/dist/suite.js +920 -513
  13. package/dist/suite.mjs +898 -523
  14. package/dist/ui.d.mts +2 -2
  15. package/dist/ui.d.ts +2 -2
  16. package/dist/ui.js +401 -66
  17. package/dist/ui.mjs +3 -1
  18. package/package.json +1 -1
  19. package/src/app/globals.css +57 -1
  20. package/src/components/ui/BrandSpinner.tsx +256 -0
  21. package/src/components/ui/CodeBlock.tsx +11 -1
  22. package/src/components/ui/Marketing.tsx +17 -1
  23. package/src/components/ui/SettingsLayout.tsx +4 -0
  24. package/src/components/ui/Table.tsx +32 -9
  25. package/src/components/ui/brand-spinner.test.tsx +62 -0
  26. package/src/components/ui/index.ts +7 -0
  27. package/src/components/ui/table.test.tsx +31 -4
  28. package/src/styles/suite-skin.css +78 -46
  29. package/src/suite/components/ai-draft-editor.tsx +184 -0
  30. package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
  31. package/src/suite/components/ai-panel.tsx +184 -14
  32. package/src/suite/components/app-switcher.tsx +13 -0
  33. package/src/suite/components/command-palette-host.tsx +20 -2
  34. package/src/suite/components/suite-notification-bell.tsx +23 -2
  35. package/src/suite/components/suite-notification-settings.tsx +11 -0
  36. package/src/suite/components/suite-settings-layout.tsx +1 -1
  37. package/src/suite/components/suite-settings-mobile-nav.tsx +5 -1
  38. package/src/suite/components/suite-sidebar.tsx +2 -2
  39. package/src/suite/components/theme-provider.tsx +2 -0
  40. package/src/suite/components/today-ui.tsx +33 -28
  41. package/src/suite/index.ts +8 -0
  42. package/src/suite/lib/suite-notifications.tsx +5 -0
@@ -26,10 +26,9 @@ import {
26
26
  } from "iconoir-react";
27
27
 
28
28
  import { Tooltip } from "../../components/ui/Tooltip";
29
- import {
30
- classifySuiteHref,
31
- type SuiteAppSlug,
32
- } from "../lib/apps";
29
+ import { trackSuiteEvent } from "../../lib/suite-analytics";
30
+ import { SuiteAiDraftEditor, suiteAiDraftFields } from "./ai-draft-editor";
31
+ import { classifySuiteHref, type SuiteAppSlug } from "../lib/apps";
33
32
 
34
33
  const SuiteAiMarkdownMessage = lazy(() => import("./ai-message-markdown"));
35
34
 
@@ -85,10 +84,7 @@ export function consumeSuiteAskAiOpenFromLocation(): boolean {
85
84
  }
86
85
 
87
86
  export type SuiteAiActionStatus =
88
- | "proposed"
89
- | "applied"
90
- | "cancelled"
91
- | "failed";
87
+ "proposed" | "applied" | "cancelled" | "failed";
92
88
 
93
89
  export interface SuiteAiAction {
94
90
  id: string;
@@ -207,6 +203,7 @@ export function SuiteAiPanel({
207
203
  resolveAction?: (
208
204
  id: string,
209
205
  decision: "confirm" | "cancel",
206
+ edits?: Record<string, unknown>,
210
207
  ) => Promise<{ messages: SuiteAiChatMessage[] }>;
211
208
  /** A durable background job is still preparing the assistant reply. */
212
209
  isWaitingForReply?: boolean;
@@ -214,6 +211,9 @@ export function SuiteAiPanel({
214
211
  const inputId = useId();
215
212
  const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
216
213
  const [resolvingId, setResolvingId] = useState("");
214
+ const [actionDrafts, setActionDrafts] = useState<
215
+ Record<string, Record<string, unknown>>
216
+ >({});
217
217
  const open = openProp ?? uncontrolledOpen;
218
218
  const setOpen = useCallback(
219
219
  (next: boolean) => {
@@ -252,6 +252,12 @@ export function SuiteAiPanel({
252
252
  return () => window.removeEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
253
253
  }, [setOpen]);
254
254
 
255
+ useEffect(() => {
256
+ if (!open) return;
257
+ trackSuiteEvent("AI Chat Opened", { origin: "suite_panel" });
258
+ trackSuiteEvent("UI Opened", { view: "ai_panel" });
259
+ }, [open]);
260
+
255
261
  const scrollToBottom = useCallback((behavior: ScrollBehavior = "auto") => {
256
262
  const el = scrollerRef.current;
257
263
  if (!el) return;
@@ -337,6 +343,14 @@ export function SuiteAiPanel({
337
343
  setPrompt("");
338
344
  setAtBottom(true);
339
345
  setMessages((prev) => [...prev, { role: "user", content: trimmed }]);
346
+ trackSuiteEvent("AI Message Sent", {
347
+ prompt_length: trimmed.length,
348
+ page_path:
349
+ typeof window !== "undefined" ? window.location.pathname : undefined,
350
+ is_preset: presets.some((preset) => preset.prompt === trimmed),
351
+ is_edit: false,
352
+ has_attachment: false,
353
+ });
340
354
  try {
341
355
  const result = await sendMessage({
342
356
  prompt: trimmed,
@@ -369,6 +383,7 @@ export function SuiteAiPanel({
369
383
  }
370
384
 
371
385
  function stop() {
386
+ trackSuiteEvent("AI Message Cancelled");
372
387
  abortRef.current?.abort();
373
388
  abortRef.current = null;
374
389
  setLoading(false);
@@ -399,6 +414,14 @@ export function SuiteAiPanel({
399
414
  setError("");
400
415
  setAtBottom(true);
401
416
  setMessages((prev) => prev.slice(0, index));
417
+ trackSuiteEvent("AI Message Sent", {
418
+ prompt_length: trimmed.length,
419
+ page_path:
420
+ typeof window !== "undefined" ? window.location.pathname : undefined,
421
+ is_preset: false,
422
+ is_edit: true,
423
+ has_attachment: false,
424
+ });
402
425
  const controller = new AbortController();
403
426
  abortRef.current = controller;
404
427
  setLoading(true);
@@ -449,6 +472,10 @@ export function SuiteAiPanel({
449
472
  await clearChat();
450
473
  setMessages([]);
451
474
  setError("");
475
+ trackSuiteEvent("AI Chat Cleared", {
476
+ page_path:
477
+ typeof window !== "undefined" ? window.location.pathname : undefined,
478
+ });
452
479
  } catch (err) {
453
480
  setError(
454
481
  err instanceof Error ? err.message : "Could not clear this chat.",
@@ -457,16 +484,25 @@ export function SuiteAiPanel({
457
484
  }
458
485
 
459
486
  async function handleResolve(
460
- actionId: string,
487
+ action: SuiteAiAction,
461
488
  decision: "confirm" | "cancel",
462
489
  ) {
463
490
  if (!resolveAction || resolvingId) return;
491
+ const actionId = action.id;
464
492
  setResolvingId(actionId);
465
493
  setError("");
466
494
  try {
467
- const result = await resolveAction(actionId, decision);
495
+ const edits = decision === "confirm" ? actionDrafts[actionId] : undefined;
496
+ const result = edits
497
+ ? await resolveAction(actionId, decision, edits)
498
+ : await resolveAction(actionId, decision);
468
499
  const nextMessages = result.messages ?? [];
469
500
  setMessages(nextMessages);
501
+ trackSuiteEvent("AI Action Resolved", {
502
+ decision,
503
+ action_kind: action.kind,
504
+ destructive: Boolean(action.payload?.destructive),
505
+ });
470
506
  notifySuiteMutated(
471
507
  nextMessages.flatMap((message) => message.actions ?? []),
472
508
  );
@@ -525,6 +561,32 @@ export function SuiteAiPanel({
525
561
  >
526
562
  <header className="flex items-center gap-2 px-2 pb-2">
527
563
  <h2 className="sr-only">{title}</h2>
564
+ {presets.length > 0 ? (
565
+ <details className="relative" data-test="ask-ai-workflows">
566
+ <summary className="ph-btn ph-btn-secondary ph-btn-sm min-h-10 cursor-pointer list-none">
567
+ Workflows
568
+ </summary>
569
+ <div className="absolute left-0 top-full z-30 mt-1 max-h-80 w-64 overflow-auto rounded-lg border border-ph-border bg-ph-surface p-1 shadow-lg">
570
+ {presets.map((preset) => (
571
+ <button
572
+ key={preset.label}
573
+ type="button"
574
+ disabled={waiting || hydrating}
575
+ className="flex min-h-10 w-full items-center rounded px-3 py-2 text-left text-sm hover:bg-ph-muted focus-visible:outline focus-visible:outline-2 disabled:opacity-50"
576
+ onClick={(event) => {
577
+ setPrompt(preset.prompt);
578
+ event.currentTarget
579
+ .closest("details")
580
+ ?.removeAttribute("open");
581
+ textareaRef.current?.focus();
582
+ }}
583
+ >
584
+ {preset.label}
585
+ </button>
586
+ ))}
587
+ </div>
588
+ </details>
589
+ ) : null}
528
590
  {messages.length > 0 ? (
529
591
  <button
530
592
  type="button"
@@ -658,7 +720,9 @@ export function SuiteAiPanel({
658
720
  <div className="mb-0.5 flex items-center gap-0.5 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100">
659
721
  <button
660
722
  type="button"
661
- onClick={() => void handleCopy(index, message.content)}
723
+ onClick={() =>
724
+ void handleCopy(index, message.content)
725
+ }
662
726
  aria-label={
663
727
  copiedIndex === index ? "Copied" : "Copy message"
664
728
  }
@@ -730,13 +794,101 @@ export function SuiteAiPanel({
730
794
  className="rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface px-3 py-2 text-sm text-ph-ink"
731
795
  >
732
796
  <p className="font-medium">{action.summary}</p>
797
+ {action.payload && action.kind.startsWith("propose_") ? (
798
+ <details
799
+ className="mt-2"
800
+ data-test="ask-ai-workflow-preview"
801
+ >
802
+ <summary className="cursor-pointer rounded text-xs text-ph-brand focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2">
803
+ Review proposed content and evidence
804
+ </summary>
805
+ {typeof action.payload.before === "string" ? (
806
+ <div className="mt-2">
807
+ <p className="text-xs font-medium text-ph-subtle">
808
+ Current description
809
+ </p>
810
+ <pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words text-xs">
811
+ {action.payload.before}
812
+ </pre>
813
+ </div>
814
+ ) : null}
815
+ <div className="mt-2">
816
+ <p className="text-xs font-medium text-ph-subtle">
817
+ Proposed draft
818
+ </p>
819
+ <pre className="mt-1 max-h-80 overflow-auto whitespace-pre-wrap break-words text-xs">
820
+ {String(
821
+ action.payload.body ??
822
+ action.payload.description ??
823
+ JSON.stringify(
824
+ action.payload.tasks ?? action.payload,
825
+ null,
826
+ 2,
827
+ ),
828
+ )}
829
+ </pre>
830
+ </div>
831
+ <pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-words text-xs text-ph-subtle">
832
+ {JSON.stringify(
833
+ action.kind === "propose_project_handover"
834
+ ? {
835
+ project_id: action.payload.project_id,
836
+ sources: action.payload.sources,
837
+ task_versions: action.payload.task_versions,
838
+ }
839
+ : action.kind === "propose_requirement_update"
840
+ ? {
841
+ source_url: action.payload.source_url,
842
+ document_revision:
843
+ action.payload.document_revision,
844
+ task_updated_at:
845
+ action.payload.task_updated_at,
846
+ }
847
+ : Object.fromEntries(
848
+ Object.entries(action.payload).filter(
849
+ ([key]) =>
850
+ ![
851
+ "body",
852
+ "description",
853
+ "before",
854
+ "tasks",
855
+ ].includes(key),
856
+ ),
857
+ ),
858
+ null,
859
+ 2,
860
+ )}
861
+ </pre>
862
+ </details>
863
+ ) : null}
733
864
  {action.status === "proposed" ? (
734
865
  <div className="mt-2 flex flex-wrap gap-2">
866
+ {suiteAiDraftFields(action.kind, action.payload) ? (
867
+ <button
868
+ type="button"
869
+ className="ph-btn ph-btn-secondary ph-btn-xs"
870
+ onClick={() =>
871
+ setActionDrafts((drafts) => ({
872
+ ...drafts,
873
+ [action.id]:
874
+ drafts[action.id] ??
875
+ suiteAiDraftFields(
876
+ action.kind,
877
+ action.payload,
878
+ )!,
879
+ }))
880
+ }
881
+ >
882
+ Edit draft
883
+ </button>
884
+ ) : null}
735
885
  <button
736
886
  type="button"
737
887
  data-test="ask-ai-action-confirm"
738
888
  disabled={Boolean(resolvingId) || !resolveAction}
739
- onClick={() => void handleResolve(action.id, "confirm")}
889
+ onClick={() =>
890
+ void handleResolve(action, "confirm")
891
+ }
740
892
  className="ph-btn ph-btn-primary ph-btn-xs"
741
893
  >
742
894
  {resolvingId === action.id ? "Working…" : "Confirm"}
@@ -745,13 +897,29 @@ export function SuiteAiPanel({
745
897
  type="button"
746
898
  data-test="ask-ai-action-cancel"
747
899
  disabled={Boolean(resolvingId) || !resolveAction}
748
- onClick={() => void handleResolve(action.id, "cancel")}
900
+ onClick={() =>
901
+ void handleResolve(action, "cancel")
902
+ }
749
903
  className="ph-btn ph-btn-secondary ph-btn-xs"
750
904
  >
751
905
  Cancel
752
906
  </button>
753
907
  </div>
754
908
  ) : null}
909
+ {action.status === "proposed" &&
910
+ actionDrafts[action.id] ? (
911
+ <SuiteAiDraftEditor
912
+ kind={action.kind}
913
+ draft={actionDrafts[action.id]}
914
+ canAddTasks={Boolean(action.payload?.project_id)}
915
+ onChange={(draft) =>
916
+ setActionDrafts((drafts) => ({
917
+ ...drafts,
918
+ [action.id]: draft,
919
+ }))
920
+ }
921
+ />
922
+ ) : null}
755
923
  {action.status === "applied" ? (
756
924
  <p className="mt-1 text-xs text-ph-subtle">
757
925
  Done
@@ -809,7 +977,9 @@ export function SuiteAiPanel({
809
977
  >
810
978
  <WarningTriangle className="mt-0.5 h-4 w-4 shrink-0 text-ph-danger" />
811
979
  <div className="min-w-0 flex-1">
812
- <p className="font-medium text-ph-danger">Something went wrong</p>
980
+ <p className="font-medium text-ph-danger">
981
+ Something went wrong
982
+ </p>
813
983
  <p className="mt-0.5 text-ph-subtle">{error}</p>
814
984
  <button
815
985
  type="button"
@@ -5,6 +5,7 @@ import { useState, type ReactNode } from "react";
5
5
  import { NavArrowDown as ChevronDown, OpenNewWindow } from "iconoir-react";
6
6
 
7
7
  import { cn } from "../lib/cn";
8
+ import { trackSuiteEvent } from "../../lib/suite-analytics";
8
9
  import type {
9
10
  SuiteDropdownItemComponent,
10
11
  SuiteDropdownMenuComponent,
@@ -173,6 +174,12 @@ export function AppSwitcher({
173
174
  onMouseDown={(e) => e.preventDefault()}
174
175
  onClick={() => {
175
176
  setOpen(false);
177
+ trackSuiteEvent('App Launched', {
178
+ target_app: app.slug,
179
+ from_app: currentApp,
180
+ new_tab: false,
181
+ origin: 'app_switcher',
182
+ });
176
183
  onSelect(app);
177
184
  }}
178
185
  className={cn(appSwitcherMenuItemClass(), "pr-11")}
@@ -202,6 +209,12 @@ export function AppSwitcher({
202
209
  onMouseDown={(e) => e.preventDefault()}
203
210
  onClick={() => {
204
211
  setOpen(false);
212
+ trackSuiteEvent('App Launched', {
213
+ target_app: app.slug,
214
+ from_app: currentApp,
215
+ new_tab: true,
216
+ origin: 'app_switcher',
217
+ });
205
218
  onSelect(app, { newTab: true });
206
219
  }}
207
220
  className="absolute right-1.5 top-1/2 z-[1] flex h-8 -translate-y-1/2 justify-center p-0 text-ph-mutedtext"
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
 
5
+ import { trackSuiteEvent } from "../../lib/suite-analytics";
5
6
  import type {
6
7
  SuiteCommandItem,
7
8
  SuiteCommandPaletteComponent,
@@ -45,6 +46,23 @@ export function CommandPaletteHost({
45
46
  }) {
46
47
  const [open, setOpen] = useState(false);
47
48
 
49
+ // Wrap selection so a palette pick reports the chosen action. Apps own the
50
+ // item list, so the id is the most stable `target` available here.
51
+ const trackedItems = useMemo<SuiteCommandItem[]>(
52
+ () =>
53
+ items.map((item) => ({
54
+ ...item,
55
+ onSelect: () => {
56
+ trackSuiteEvent("Command Palette Action", {
57
+ action_kind: "command",
58
+ target: item.id,
59
+ });
60
+ item.onSelect();
61
+ },
62
+ })),
63
+ [items],
64
+ );
65
+
48
66
  useEffect(() => {
49
67
  const onKey = (e: KeyboardEvent) => {
50
68
  if (!((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k")) return;
@@ -61,7 +79,7 @@ export function CommandPaletteHost({
61
79
 
62
80
  return (
63
81
  <CommandPalette
64
- items={items}
82
+ items={trackedItems}
65
83
  isOpen={open}
66
84
  onClose={() => setOpen(false)}
67
85
  placeholder={placeholder}
@@ -3,6 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import { Bell } from "iconoir-react";
5
5
 
6
+ import { trackSuiteEvent } from "../../lib/suite-analytics";
6
7
  import type { SuiteDropdownMenuComponent } from "../lib/injected";
7
8
  import { useSuiteNotifications } from "../lib/suite-notifications";
8
9
 
@@ -50,7 +51,14 @@ export function SuiteNotificationBell({
50
51
 
51
52
  const onOpenChange = (next: boolean) => {
52
53
  setOpen(next);
53
- if (next) refresh();
54
+ if (next) {
55
+ trackSuiteEvent("UI Opened", {
56
+ view: "notification_bell",
57
+ kind: "notifications",
58
+ unread_count: unread,
59
+ });
60
+ refresh();
61
+ }
54
62
  };
55
63
 
56
64
  return (
@@ -84,7 +92,13 @@ export function SuiteNotificationBell({
84
92
  type="button"
85
93
  id={`${dataTest}-mark-all`}
86
94
  data-test={`${dataTest}-mark-all`}
87
- onClick={markAllRead}
95
+ onClick={() => {
96
+ trackSuiteEvent("Notification Read", {
97
+ operation: "all",
98
+ count: unread,
99
+ });
100
+ markAllRead();
101
+ }}
88
102
  className="ph-btn ph-btn-link ph-btn-xs text-xs"
89
103
  >
90
104
  Mark all read
@@ -104,6 +118,13 @@ export function SuiteNotificationBell({
104
118
  data-test={`${dataTest}-item-${n.id}`}
105
119
  onClick={() => {
106
120
  setOpen(false);
121
+ trackSuiteEvent("Notification Read", {
122
+ operation: "read",
123
+ notification_id: n.id,
124
+ kind: n.kind,
125
+ source_app: n.source_app,
126
+ unread: !n.read_at,
127
+ });
107
128
  select(n);
108
129
  }}
109
130
  className="ph-dropdown-item flex w-full flex-col items-stretch gap-0.5 px-3 py-2 text-left"
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import { Alert, Badge, Button, Card } from "@/components/ui";
5
+ import { trackSuiteEvent } from "../../lib/suite-analytics";
5
6
 
6
7
  export type SuiteWebPushState =
7
8
  | "unsupported"
@@ -73,6 +74,11 @@ export function SuiteNotificationSettings({
73
74
  try {
74
75
  await controller.enable();
75
76
  await refresh();
77
+ trackSuiteEvent("Push Notifications Configured", {
78
+ enabled: true,
79
+ permission:
80
+ typeof Notification !== "undefined" ? Notification.permission : "unknown",
81
+ });
76
82
  } catch (err) {
77
83
  setError(
78
84
  err instanceof Error ? err.message : "Could not enable notifications.",
@@ -89,6 +95,11 @@ export function SuiteNotificationSettings({
89
95
  try {
90
96
  await controller.disable();
91
97
  await refresh();
98
+ trackSuiteEvent("Push Notifications Configured", {
99
+ enabled: false,
100
+ permission:
101
+ typeof Notification !== "undefined" ? Notification.permission : "unknown",
102
+ });
92
103
  } catch (err) {
93
104
  setError(
94
105
  err instanceof Error ? err.message : "Could not disable notifications.",
@@ -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="suite-settings-sidebar hidden w-56 shrink-0 lg:block lg:self-stretch lg:overflow-y-auto lg:overscroll-contain"
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 {
@@ -62,7 +63,10 @@ export function SuiteSettingsMobileNav({
62
63
  type="button"
63
64
  id={item.id}
64
65
  data-test={item.testId}
65
- onClick={() => onSelect(item.href)}
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
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)]",
@@ -277,9 +277,9 @@ export function SuiteSidebar({
277
277
  aria-current={active ? "page" : undefined}
278
278
  onClick={item.onClick}
279
279
  className={cn(
280
- "group relative flex min-h-11 touch-manipulation items-center gap-2.5 rounded-[var(--ph-radius-control)] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)]",
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",
281
281
  active
282
- ? "bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)] shadow-[inset_0_0_0_1px_var(--ph-selected-border)]"
282
+ ? "bg-[var(--ph-selected-bg)] text-[var(--ph-selected-text)]"
283
283
  : "text-ph-subtle hover:bg-[var(--ph-surface-hover)] hover:text-ph-ink",
284
284
  collapsed
285
285
  ? "mx-auto size-11 shrink-0 justify-center gap-0 px-0 py-0"
@@ -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
  );