@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.
- package/dist/{Chip-DKT-82Lf.d.mts → Chip-Dw2g2t8D.d.mts} +14 -2
- package/dist/{Chip-DKT-82Lf.d.ts → Chip-Dw2g2t8D.d.ts} +14 -2
- package/dist/{chunk-LHE4ROON.mjs → chunk-RIPGN6FG.mjs} +412 -61
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +355 -23
- package/dist/index.mjs +11 -3
- package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
- package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
- package/dist/suite.d.mts +14 -4
- package/dist/suite.d.ts +14 -4
- package/dist/suite.js +920 -513
- package/dist/suite.mjs +898 -523
- package/dist/ui.d.mts +2 -2
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +401 -66
- package/dist/ui.mjs +3 -1
- package/package.json +1 -1
- package/src/app/globals.css +57 -1
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/CodeBlock.tsx +11 -1
- package/src/components/ui/Marketing.tsx +17 -1
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/Table.tsx +32 -9
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +7 -0
- package/src/components/ui/table.test.tsx +31 -4
- package/src/styles/suite-skin.css +78 -46
- package/src/suite/components/ai-draft-editor.tsx +184 -0
- package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
- package/src/suite/components/ai-panel.tsx +184 -14
- package/src/suite/components/app-switcher.tsx +13 -0
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-notification-bell.tsx +23 -2
- package/src/suite/components/suite-notification-settings.tsx +11 -0
- package/src/suite/components/suite-settings-layout.tsx +1 -1
- package/src/suite/components/suite-settings-mobile-nav.tsx +5 -1
- package/src/suite/components/suite-sidebar.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-ui.tsx +33 -28
- package/src/suite/index.ts +8 -0
- 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
|
-
|
|
31
|
-
|
|
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
|
-
| "
|
|
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
|
-
|
|
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
|
|
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={() =>
|
|
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={() =>
|
|
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={() =>
|
|
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">
|
|
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={
|
|
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)
|
|
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={
|
|
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-
|
|
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={() =>
|
|
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
|
|
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)]
|
|
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
|
);
|