@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.
Files changed (79) hide show
  1. package/dist/{Chip-BzuNgJIa.d.mts → Chip-Dw2g2t8D.d.mts} +18 -4
  2. package/dist/{Chip-BzuNgJIa.d.ts → Chip-Dw2g2t8D.d.ts} +18 -4
  3. package/dist/{chunk-6SVJAU5K.mjs → chunk-RIPGN6FG.mjs} +1333 -526
  4. package/dist/index.d.mts +3 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +490 -119
  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 +36 -4
  11. package/dist/suite.d.ts +36 -4
  12. package/dist/suite.js +4617 -4001
  13. package/dist/suite.mjs +1456 -868
  14. package/dist/tailwind-preset.js +10 -1
  15. package/dist/tailwind-preset.mjs +10 -1
  16. package/dist/ui.d.mts +33 -4
  17. package/dist/ui.d.ts +33 -4
  18. package/dist/ui.js +1364 -571
  19. package/dist/ui.mjs +15 -1
  20. package/docs/macos-identity-audit.md +279 -0
  21. package/package.json +1 -1
  22. package/src/app/globals.css +390 -44
  23. package/src/components/ui/Accordion.tsx +4 -7
  24. package/src/components/ui/Avatar.tsx +5 -5
  25. package/src/components/ui/BrandSpinner.tsx +256 -0
  26. package/src/components/ui/Calendar.tsx +7 -7
  27. package/src/components/ui/CodeBlock.tsx +14 -11
  28. package/src/components/ui/CollapsibleSection.tsx +4 -4
  29. package/src/components/ui/CommandPalette.tsx +7 -10
  30. package/src/components/ui/EmptyState.tsx +2 -2
  31. package/src/components/ui/Input.tsx +1 -1
  32. package/src/components/ui/Marketing.tsx +144 -0
  33. package/src/components/ui/MarketingIllustrations.tsx +287 -0
  34. package/src/components/ui/Panel.tsx +1 -1
  35. package/src/components/ui/SegmentedControl.tsx +59 -13
  36. package/src/components/ui/SettingsLayout.tsx +4 -0
  37. package/src/components/ui/ShowcaseWrapper.tsx +3 -3
  38. package/src/components/ui/Table.tsx +45 -10
  39. package/src/components/ui/Typography.tsx +4 -4
  40. package/src/components/ui/avatar-stat.test.tsx +14 -1
  41. package/src/components/ui/brand-spinner.test.tsx +62 -0
  42. package/src/components/ui/index.ts +19 -11
  43. package/src/components/ui/interaction-primitives.test.tsx +12 -6
  44. package/src/components/ui/marketing.test.tsx +39 -0
  45. package/src/components/ui/table.test.tsx +47 -0
  46. package/src/styles/suite-skin.css +471 -74
  47. package/src/styles/themes.css +45 -45
  48. package/src/styles/typography.css +25 -1
  49. package/src/suite/components/ai-draft-editor.tsx +184 -0
  50. package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
  51. package/src/suite/components/ai-panel.tsx +201 -31
  52. package/src/suite/components/app-switcher.tsx +22 -9
  53. package/src/suite/components/command-palette-host.tsx +20 -2
  54. package/src/suite/components/suite-app-layout.tsx +7 -4
  55. package/src/suite/components/suite-bottom-nav.tsx +1 -1
  56. package/src/suite/components/suite-clients-projects-directory.tsx +6 -6
  57. package/src/suite/components/suite-integration-picker.tsx +7 -7
  58. package/src/suite/components/suite-integration-rules.tsx +5 -5
  59. package/src/suite/components/suite-layout.test.tsx +32 -3
  60. package/src/suite/components/suite-layout.tsx +4 -4
  61. package/src/suite/components/suite-mobile-drawer.tsx +2 -2
  62. package/src/suite/components/suite-mobile-header.tsx +1 -1
  63. package/src/suite/components/suite-notification-bell.tsx +27 -6
  64. package/src/suite/components/suite-notification-settings.tsx +182 -0
  65. package/src/suite/components/suite-palette-library.test.tsx +46 -0
  66. package/src/suite/components/suite-palette-library.tsx +82 -15
  67. package/src/suite/components/suite-settings-layout.tsx +2 -2
  68. package/src/suite/components/suite-settings-mobile-nav.tsx +10 -10
  69. package/src/suite/components/suite-sidebar.test.tsx +39 -0
  70. package/src/suite/components/suite-sidebar.tsx +41 -19
  71. package/src/suite/components/suite-skeleton.tsx +6 -6
  72. package/src/suite/components/suite-user-menu.tsx +2 -2
  73. package/src/suite/components/theme-provider.tsx +2 -0
  74. package/src/suite/components/today-calibrating.tsx +3 -3
  75. package/src/suite/components/today-ui.tsx +42 -37
  76. package/src/suite/components/workspace-switcher.tsx +3 -3
  77. package/src/suite/index.ts +14 -0
  78. package/src/suite/lib/suite-notifications.tsx +5 -0
  79. package/tailwind-preset.ts +9 -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,12 +561,38 @@ 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"
531
593
  onClick={() => void handleClear()}
532
594
  disabled={waiting}
533
- 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"
595
+ className="ph-btn ph-btn-ghost ph-btn-sm ml-auto h-9 text-xs text-ph-subtle"
534
596
  data-test="ask-ai-clear"
535
597
  >
536
598
  <Erase className="h-3.5 w-3.5" />
@@ -558,7 +620,7 @@ export function SuiteAiPanel({
558
620
 
559
621
  {!hydrating && messages.length === 0 && !error ? (
560
622
  <div className="mx-auto mt-4 flex max-w-sm flex-col items-center px-1 text-center">
561
- <span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-ph-brand/10 text-ph-brand">
623
+ <span className="flex h-10 w-10 items-center justify-center rounded-[var(--ph-radius-surface)] bg-ph-brand/10 text-ph-brand">
562
624
  <Icon className="h-5 w-5" />
563
625
  </span>
564
626
  <p className="mt-3 font-display text-base font-semibold text-ph-ink">
@@ -575,7 +637,7 @@ export function SuiteAiPanel({
575
637
  type="button"
576
638
  onClick={() => void ask(preset.prompt)}
577
639
  disabled={waiting || hydrating}
578
- 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"
640
+ className="group flex w-full items-center gap-2 rounded-[var(--ph-radius-surface)] border border-ph-border bg-ph-surface px-3 py-2 text-left text-sm text-ph-ink transition-[background-color,border-color,box-shadow] hover:border-[var(--ph-selected-border)] hover:bg-[var(--ph-surface-hover)] focus-visible:outline-none focus-visible:shadow-[var(--ph-focus-halo)] disabled:opacity-40"
579
641
  >
580
642
  <Sparks className="h-4 w-4 shrink-0 text-ph-brand" />
581
643
  <span className="min-w-0 flex-1 truncate">
@@ -620,7 +682,7 @@ export function SuiteAiPanel({
620
682
  }}
621
683
  rows={2}
622
684
  data-test="ask-ai-edit-input"
623
- className="w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-ph-brand/45 focus-visible:ring-2 focus-visible:ring-ph-focus/35"
685
+ className="w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-[var(--ph-selected-border)] focus-visible:shadow-[var(--ph-focus-halo)]"
624
686
  />
625
687
  <div className="flex justify-end gap-2">
626
688
  <button
@@ -629,7 +691,7 @@ export function SuiteAiPanel({
629
691
  setEditingIndex(null);
630
692
  setEditDraft("");
631
693
  }}
632
- className="inline-flex min-h-8 items-center 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"
694
+ className="ph-btn ph-btn-secondary ph-btn-xs"
633
695
  >
634
696
  Cancel
635
697
  </button>
@@ -638,7 +700,7 @@ export function SuiteAiPanel({
638
700
  data-test="ask-ai-edit-save"
639
701
  disabled={!editDraft.trim() || waiting}
640
702
  onClick={() => void saveEdit(index)}
641
- className="inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
703
+ className="ph-btn ph-btn-primary ph-btn-xs"
642
704
  >
643
705
  Save
644
706
  </button>
@@ -658,12 +720,14 @@ 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
  }
665
729
  data-test="ask-ai-copy"
666
- className="flex h-7 w-7 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-focus"
730
+ className="ph-btn ph-btn-ghost ph-btn-square ph-btn-xs h-7 w-7 text-ph-mutedtext"
667
731
  >
668
732
  {copiedIndex === index ? (
669
733
  <Check className="h-3.5 w-3.5" />
@@ -679,7 +743,7 @@ export function SuiteAiPanel({
679
743
  }}
680
744
  aria-label="Edit message"
681
745
  data-test="ask-ai-edit"
682
- className="flex h-7 w-7 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-focus"
746
+ className="ph-btn ph-btn-ghost ph-btn-square ph-btn-xs h-7 w-7 text-ph-mutedtext"
683
747
  >
684
748
  <EditPencil className="h-3.5 w-3.5" />
685
749
  </button>
@@ -727,17 +791,105 @@ export function SuiteAiPanel({
727
791
  key={action.id}
728
792
  data-test="ask-ai-action"
729
793
  data-status={action.status}
730
- className="rounded-xl border border-ph-border bg-ph-surface px-3 py-2 text-sm text-ph-ink"
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")}
740
- className="inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
889
+ onClick={() =>
890
+ void handleResolve(action, "confirm")
891
+ }
892
+ className="ph-btn ph-btn-primary ph-btn-xs"
741
893
  >
742
894
  {resolvingId === action.id ? "Working…" : "Confirm"}
743
895
  </button>
@@ -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")}
749
- className="inline-flex min-h-8 items-center 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"
900
+ onClick={() =>
901
+ void handleResolve(action, "cancel")
902
+ }
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
@@ -805,17 +973,19 @@ export function SuiteAiPanel({
805
973
  {error ? (
806
974
  <div
807
975
  role="alert"
808
- className="flex items-start gap-2 rounded-xl border border-ph-danger/30 bg-ph-danger/10 px-3 py-2.5 text-sm text-ph-ink"
976
+ className="flex items-start gap-2 rounded-[var(--ph-radius-surface)] border border-ph-danger/30 bg-ph-danger/10 px-3 py-2.5 text-sm text-ph-ink"
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"
816
986
  onClick={retry}
817
987
  disabled={waiting}
818
- 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"
988
+ className="ph-btn ph-btn-secondary ph-btn-xs mt-2 gap-1.5"
819
989
  >
820
990
  Try again
821
991
  </button>
@@ -830,7 +1000,7 @@ export function SuiteAiPanel({
830
1000
  <button
831
1001
  type="button"
832
1002
  onClick={() => scrollToBottom("smooth")}
833
- className="flex h-9 w-9 items-center justify-center rounded-full border border-ph-border bg-ph-surface text-ph-subtle shadow-ph-md transition-colors hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
1003
+ className="ph-btn ph-btn-ghost ph-btn-circle h-9 w-9 border border-ph-border bg-ph-surface text-ph-subtle shadow-[var(--ph-shadow-floating)]"
834
1004
  aria-label="Scroll to latest"
835
1005
  >
836
1006
  <ArrowDown className="h-4 w-4" />
@@ -841,7 +1011,7 @@ export function SuiteAiPanel({
841
1011
  </div>
842
1012
 
843
1013
  <div className="px-2 pt-2 pb-[max(2rem,calc(env(safe-area-inset-bottom)+1rem))]">
844
- <div className="flex items-end gap-1.5 rounded-2xl border border-ph-border bg-ph-surface px-1.5 py-1 transition-colors focus-within:border-ph-brand/45 focus-within:ring-2 focus-within:ring-ph-focus/35">
1014
+ <div className="suite-ai-composer flex items-end gap-1.5 border bg-ph-surface px-1.5 py-1">
845
1015
  <label htmlFor={inputId} className="sr-only">
846
1016
  Ask a question
847
1017
  </label>
@@ -871,7 +1041,7 @@ export function SuiteAiPanel({
871
1041
  type="button"
872
1042
  onClick={stop}
873
1043
  data-test="ask-ai-stop"
874
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-muted text-ph-ink transition-colors hover:bg-ph-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
1044
+ className="ph-btn ph-btn-ghost ph-btn-circle h-9 w-9 shrink-0 bg-ph-muted text-ph-ink"
875
1045
  aria-label="Stop generating"
876
1046
  >
877
1047
  <Square className="h-3.5 w-3.5 fill-current" />
@@ -884,7 +1054,7 @@ export function SuiteAiPanel({
884
1054
  onClick={() => void ask(prompt)}
885
1055
  disabled={!prompt.trim()}
886
1056
  data-test="ask-ai-send"
887
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-brand text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
1057
+ className="ph-btn ph-btn-primary ph-btn-circle h-9 w-9 shrink-0"
888
1058
  aria-label="Send message"
889
1059
  >
890
1060
  <ArrowUp className="h-4 w-4" />
@@ -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,
@@ -12,20 +13,20 @@ import type {
12
13
 
13
14
  export function appSwitcherTriggerClass(open: boolean, collapsed = false) {
14
15
  return cn(
15
- "group flex min-w-0 items-center gap-2 rounded-lg border px-2 py-1.5 text-left transition-all duration-200",
16
+ "group flex min-w-0 items-center gap-2 rounded-[var(--ph-radius-control)] border px-2 py-1.5 text-left transition-[color,background-color,border-color,box-shadow] duration-150",
16
17
  collapsed ? "h-11 w-11 justify-center px-0 py-0" : "w-full",
17
18
  open
18
- ? "border-ph-brand/30 bg-ph-muted shadow-[0_1px_2px_rgba(0,0,0,0.06)] ring-1 ring-ph-brand/15"
19
- : "border-transparent hover:border-ph-border hover:bg-ph-muted/80 hover:shadow-sm",
19
+ ? "border-[var(--ph-selected-border)] bg-[var(--ph-selected-bg)] shadow-[var(--ph-shadow-control)]"
20
+ : "border-transparent hover:border-ph-border hover:bg-[var(--ph-surface-hover)]",
20
21
  );
21
22
  }
22
23
 
23
24
  export function appSwitcherMarkClass() {
24
- return "h-8 w-8 shrink-0 rounded-lg";
25
+ return "h-8 w-8 shrink-0 rounded-[var(--ph-radius-control)]";
25
26
  }
26
27
 
27
28
  export function appSwitcherMenuItemClass() {
28
- return "rounded-lg px-2 py-2 data-[highlighted]:bg-transparent hover:bg-ph-muted/80 focus-visible:shadow-none";
29
+ return "px-2 py-2";
29
30
  }
30
31
 
31
32
  export function AppSwitcherMark({
@@ -58,7 +59,7 @@ export function AppSwitcherChevron({
58
59
  return (
59
60
  <span
60
61
  className={cn(
61
- "absolute bottom-0 right-0 flex h-3 w-3 items-center justify-center rounded-full border border-ph-border bg-ph-surface shadow-sm transition-colors",
62
+ "absolute bottom-0 right-0 flex h-3 w-3 items-center justify-center rounded-full border border-ph-border bg-ph-surface transition-colors",
62
63
  open && "border-ph-brand/40 bg-ph-brand/10",
63
64
  )}
64
65
  aria-hidden
@@ -76,7 +77,7 @@ export function AppSwitcherChevron({
76
77
  return (
77
78
  <span
78
79
  className={cn(
79
- "border-ph-border/80 flex h-5 w-5 shrink-0 items-center justify-center rounded-lg border bg-ph-canvas shadow-sm transition-all duration-200",
80
+ "border-ph-border/80 flex h-5 w-5 shrink-0 items-center justify-center rounded-[var(--ph-radius-control)] border bg-ph-canvas transition-[color,background-color,border-color] duration-150",
80
81
  open
81
82
  ? "border-ph-brand/35 bg-ph-brand/10 text-ph-brand"
82
83
  : "text-ph-mutedtext group-hover:border-ph-border group-hover:bg-ph-surface",
@@ -160,7 +161,7 @@ export function AppSwitcher({
160
161
  }
161
162
  >
162
163
  <div id="app-switcher-menu" className="p-1" data-test="app-switcher-menu">
163
- <p className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-ph-mutedtext">
164
+ <p className="px-3 py-2 text-[length:var(--ph-type-overline)] font-semibold uppercase tracking-[0.08em] text-ph-mutedtext">
164
165
  Switch application
165
166
  </p>
166
167
  {apps
@@ -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,9 +209,15 @@ 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
- 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"
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"
208
221
  style={{ width: "2rem" }}
209
222
  >
210
223
  <OpenNewWindow className="h-4 w-4" aria-hidden="true" />
@@ -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}
@@ -59,7 +59,7 @@ export function SuiteAppLayout({
59
59
  >
60
60
  <aside
61
61
  aria-label="Sidebar"
62
- className="relative hidden shrink-0 flex-col overflow-visible border-r border-ph-border ease-out lg:flex"
62
+ className="suite-app-layout__sidebar relative hidden shrink-0 flex-col overflow-visible border-r ease-out lg:flex"
63
63
  style={{ width: `${sidebarWidth}px` }}
64
64
  data-collapsed={collapsed ? "true" : "false"}
65
65
  >
@@ -81,7 +81,10 @@ export function SuiteAppLayout({
81
81
  onKeyDown={(event) => {
82
82
  if (event.key === "Home") {
83
83
  event.preventDefault();
84
- onResetWidth?.();
84
+ onResizeBy?.(SIDEBAR_RAIL_WIDTH - sidebarWidth);
85
+ } else if (event.key === "End") {
86
+ event.preventDefault();
87
+ onResizeBy?.(SIDEBAR_MAX_WIDTH - sidebarWidth);
85
88
  } else if (event.key === "ArrowLeft") {
86
89
  event.preventDefault();
87
90
  onResizeBy?.(-16);
@@ -90,7 +93,7 @@ export function SuiteAppLayout({
90
93
  onResizeBy?.(16);
91
94
  }
92
95
  }}
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)]"
96
+ className="suite-sidebar-resize-handle absolute inset-y-0 right-0 z-20 w-2 cursor-col-resize touch-none before:absolute before:-left-2 before:-right-2 before:inset-y-0 before:content-['']"
94
97
  />
95
98
  ) : null}
96
99
  </aside>
@@ -104,7 +107,7 @@ export function SuiteAppLayout({
104
107
  tabIndex={0}
105
108
  data-lock-scroll={lockMainScroll ? "true" : undefined}
106
109
  className={cn(
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",
110
+ "flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none",
108
111
  "suite-app-layout__main",
109
112
  lockMainScroll ? "overflow-hidden" : "overflow-y-auto",
110
113
  )}
@@ -89,7 +89,7 @@ export function SuiteBottomNav({
89
89
  }
90
90
  aria-current={active ? "page" : undefined}
91
91
  className={cn(
92
- "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",
92
+ "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-[length:var(--ph-type-overline)] font-medium leading-none text-ph-mutedtext transition-colors duration-200 focus-visible:outline-none focus-visible:shadow-[inset_0_0_0_3px_color-mix(in_oklab,var(--ph-focus-ring)_30%,transparent)] motion-reduce:transition-none",
93
93
  active && "font-semibold text-ph-brand",
94
94
  )}
95
95
  >