@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
@@ -62,20 +62,56 @@ body {
62
62
  }
63
63
 
64
64
  /* Shared app canvas. Dense workspaces do not paint a decorative background. */
65
- /* One centred marketing header and one button face across the product family. */
65
+ /* One centred marketing header and one button face across the product family.
66
+ The header stays transparent; scrolling content blurs and fades behind it,
67
+ the same treatment as the Today title and the blog/guide chrome. */
68
+ .marketing-header,
69
+ .landing-page-nav {
70
+ isolation: isolate;
71
+ background: transparent;
72
+ border-bottom: none;
73
+ }
74
+
66
75
  .marketing-header {
67
76
  position: sticky;
68
77
  top: 0;
69
78
  z-index: 50;
70
- border-bottom: 1px solid var(--ph-border-subtle);
71
- background: color-mix(in oklab, var(--ph-canvas) 88%, transparent);
72
- -webkit-backdrop-filter: blur(16px) saturate(1.15);
73
- backdrop-filter: blur(16px) saturate(1.15);
74
79
  }
75
- @supports not (backdrop-filter: blur(1px)) {
76
- .marketing-header {
77
- background: var(--ph-canvas);
78
- }
80
+
81
+ .marketing-header::before,
82
+ .landing-page-nav::before {
83
+ content: "";
84
+ position: absolute;
85
+ inset-inline: 0;
86
+ top: 0;
87
+ height: calc(100% + var(--suite-scroll-fade));
88
+ z-index: -1;
89
+ /* No surface fill: the page background shows through from the very top,
90
+ and only passing content is blurred. Keeps the header from reading as a
91
+ lighter bar. */
92
+ background: transparent;
93
+ -webkit-backdrop-filter: blur(8px);
94
+ backdrop-filter: blur(8px);
95
+ -webkit-mask-image: linear-gradient(
96
+ to bottom,
97
+ #000 calc(100% - var(--suite-scroll-fade)),
98
+ transparent
99
+ );
100
+ mask-image: linear-gradient(
101
+ to bottom,
102
+ #000 calc(100% - var(--suite-scroll-fade)),
103
+ transparent
104
+ );
105
+ pointer-events: none;
106
+ }
107
+
108
+ /* At rest the header is fully transparent so the page background starts at the
109
+ top. The frost only kicks in once content scrolls beneath it. */
110
+ .marketing-header[data-scrolled="false"]::before {
111
+ -webkit-backdrop-filter: none;
112
+ backdrop-filter: none;
113
+ -webkit-mask-image: none;
114
+ mask-image: none;
79
115
  }
80
116
  .marketing-header__inner {
81
117
  display: flex;
@@ -269,8 +305,10 @@ body {
269
305
  .marketing-landing::before {
270
306
  content: "";
271
307
  position: absolute;
272
- inset: 5rem 0 auto;
273
- height: 70rem;
308
+ /* Start at the very top so the atmosphere is behind the header too — the
309
+ header is transparent, so an inset here would read as a bare strip. */
310
+ inset: 0 0 auto;
311
+ height: 75rem;
274
312
  z-index: -1;
275
313
  pointer-events: none;
276
314
  background:
@@ -458,7 +496,7 @@ html[data-marketing-paused] .marketing-landing::before {
458
496
  }
459
497
 
460
498
  .suite-settings-sidebar {
461
- padding: 0.375rem;
499
+ padding: 0.375rem 0.375rem 0.75rem;
462
500
  border: 1px solid var(--ph-border-subtle);
463
501
  border-radius: var(--ph-radius-surface);
464
502
  background: var(--ph-surface-inset);
@@ -480,6 +518,33 @@ html[data-marketing-paused] .marketing-landing::before {
480
518
  .today-page-frame .suite-page-inset {
481
519
  padding-top: var(--suite-today-pad-y-top);
482
520
  }
521
+
522
+ /* Start the body below the sticky header's fade, with a clear 16px gap. */
523
+ .today-page-frame .suite-page-header + .suite-page-body {
524
+ margin-top: calc(var(--suite-scroll-fade) + var(--suite-space-4));
525
+ }
526
+
527
+ .today-work-row {
528
+ display: flex;
529
+ min-height: 2.75rem;
530
+ min-width: 0;
531
+ align-items: flex-start;
532
+ gap: 0.75rem;
533
+ padding: 0.875rem 1rem;
534
+ border-radius: var(--ph-radius-control);
535
+ transition: background-color var(--ph-motion-fast) ease;
536
+ }
537
+
538
+ :is(a, button).today-work-row:hover {
539
+ background: var(--ph-surface-hover);
540
+ }
541
+
542
+ :is(a, button).today-work-row:focus-visible {
543
+ outline: none;
544
+ box-shadow: inset 0 0 0 3px
545
+ color-mix(in oklab, var(--ph-focus-ring) 30%, transparent);
546
+ }
547
+
483
548
  @media (min-width: 640px) {
484
549
  .today-page-frame .suite-page-inset {
485
550
  padding-top: var(--suite-today-pad-y-top-sm);
@@ -734,40 +799,7 @@ html.dark .today-page-frame__cloud--2 {
734
799
  }
735
800
  }
736
801
 
737
- /* Public landing nav — same sit-on-the-page fade as Today. No bar, no border;
738
- the page background shows through and only scrolling content dissolves. */
739
- .landing-page-nav {
740
- isolation: isolate;
741
- background: transparent;
742
- border-bottom: none;
743
- }
744
- .landing-page-nav::before {
745
- content: "";
746
- position: absolute;
747
- inset-inline: 0;
748
- top: 0;
749
- height: calc(100% + var(--suite-scroll-fade));
750
- z-index: -1;
751
- background: linear-gradient(
752
- to bottom,
753
- color-mix(in srgb, var(--ph-canvas) 18%, transparent)
754
- calc(100% - var(--suite-scroll-fade)),
755
- transparent
756
- );
757
- -webkit-backdrop-filter: blur(8px);
758
- backdrop-filter: blur(8px);
759
- -webkit-mask-image: linear-gradient(
760
- to bottom,
761
- #000 calc(100% - var(--suite-scroll-fade)),
762
- transparent
763
- );
764
- mask-image: linear-gradient(
765
- to bottom,
766
- #000 calc(100% - var(--suite-scroll-fade)),
767
- transparent
768
- );
769
- pointer-events: none;
770
- }
802
+ /* Public landing nav (see the shared header rule above for its fade layer). */
771
803
 
772
804
  /* Page stage — ONE layout standard for every suite app.
773
805
  - Default width="full": use the full main column (no max-width centring)
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ const EDITABLE_FIELDS: Record<string, string[]> = {
4
+ propose_project_handover: ["title", "body"],
5
+ propose_requirement_update: ["description"],
6
+ propose_source_tasks: ["tasks"],
7
+ propose_source_task_update: ["description"],
8
+ propose_knowledge_pack: ["title", "body", "tasks"],
9
+ propose_document_update: ["body"],
10
+ };
11
+
12
+ export function suiteAiDraftFields(
13
+ kind: string,
14
+ payload?: Record<string, unknown>,
15
+ ): Record<string, unknown> | null {
16
+ const fields = EDITABLE_FIELDS[kind];
17
+ return fields && payload
18
+ ? Object.fromEntries(
19
+ fields.map((key) => [key, payload[key] ?? (key === "tasks" ? [] : "")]),
20
+ )
21
+ : null;
22
+ }
23
+
24
+ export function SuiteAiDraftEditor({
25
+ kind,
26
+ draft,
27
+ canAddTasks,
28
+ onChange,
29
+ }: {
30
+ kind: string;
31
+ draft: Record<string, unknown>;
32
+ canAddTasks: boolean;
33
+ onChange: (draft: Record<string, unknown>) => void;
34
+ }) {
35
+ const change = (key: string, value: unknown) =>
36
+ onChange({ ...draft, [key]: value });
37
+ const tasks = Array.isArray(draft.tasks)
38
+ ? (draft.tasks as Record<string, unknown>[])
39
+ : [];
40
+ const editTask = (index: number, key: string, value: unknown) =>
41
+ change(
42
+ "tasks",
43
+ tasks.map((task, i) => (i === index ? { ...task, [key]: value } : task)),
44
+ );
45
+ return (
46
+ <div
47
+ className="mt-3 space-y-3 rounded-lg border border-ph-border bg-ph-canvas p-3"
48
+ data-test="ai-draft-editor"
49
+ >
50
+ <p className="text-xs text-ph-mutedtext">
51
+ Edit draft content. Source evidence, versions and destination remain
52
+ protected.
53
+ </p>
54
+ {Object.entries(draft)
55
+ .filter(([key]) => key !== "tasks")
56
+ .map(([key, value]) => (
57
+ <label key={key} className="block space-y-1 text-sm">
58
+ <span className="font-medium">
59
+ {key === "title" ? "Title" : "Proposed content"}
60
+ </span>
61
+ {key === "title" ? (
62
+ <input
63
+ className="ph-input min-h-11 w-full"
64
+ value={String(value)}
65
+ maxLength={255}
66
+ onChange={(event) => change(key, event.target.value)}
67
+ />
68
+ ) : (
69
+ <textarea
70
+ className="ph-input min-h-40 w-full"
71
+ value={String(value)}
72
+ maxLength={kind === "propose_project_handover" ? 16000 : 12000}
73
+ onChange={(event) => change(key, event.target.value)}
74
+ />
75
+ )}
76
+ </label>
77
+ ))}
78
+ {"tasks" in draft ? (
79
+ <div className="space-y-3">
80
+ <p className="text-sm font-medium">Task drafts</p>
81
+ {tasks.map((task, index) => (
82
+ <fieldset
83
+ key={index}
84
+ className="space-y-2 rounded border border-ph-border p-2"
85
+ >
86
+ <legend className="px-1 text-xs">Task {index + 1}</legend>
87
+ <label className="block text-sm">
88
+ Title
89
+ <input
90
+ className="ph-input min-h-11 w-full"
91
+ value={String(task.title ?? "")}
92
+ maxLength={255}
93
+ onChange={(event) =>
94
+ editTask(index, "title", event.target.value)
95
+ }
96
+ />
97
+ </label>
98
+ <label className="block text-sm">
99
+ Description
100
+ <textarea
101
+ className="ph-input min-h-24 w-full"
102
+ value={String(task.description ?? "")}
103
+ maxLength={10000}
104
+ onChange={(event) =>
105
+ editTask(index, "description", event.target.value)
106
+ }
107
+ />
108
+ </label>
109
+ <div className="flex flex-wrap gap-2">
110
+ <label className="text-sm">
111
+ Due date
112
+ <input
113
+ type="date"
114
+ className="ph-input min-h-11 block"
115
+ value={String(task.due_date ?? "")}
116
+ onChange={(event) =>
117
+ editTask(index, "due_date", event.target.value || null)
118
+ }
119
+ />
120
+ </label>
121
+ <label className="text-sm">
122
+ Priority
123
+ <select
124
+ className="ph-input min-h-11 block"
125
+ value={String(task.priority ?? "low")}
126
+ onChange={(event) =>
127
+ editTask(index, "priority", event.target.value)
128
+ }
129
+ >
130
+ {["low", "medium", "high", "urgent"].map((value) => (
131
+ <option key={value}>{value}</option>
132
+ ))}
133
+ </select>
134
+ </label>
135
+ </div>
136
+ <label className="flex min-h-11 items-center gap-2 text-sm">
137
+ <input
138
+ type="checkbox"
139
+ checked={Boolean(task.assign_me)}
140
+ onChange={(event) =>
141
+ editTask(index, "assign_me", event.target.checked)
142
+ }
143
+ />
144
+ Assign to me
145
+ </label>
146
+ <button
147
+ type="button"
148
+ className="ph-btn ph-btn-secondary ph-btn-xs"
149
+ disabled={kind === "propose_source_tasks" && tasks.length === 1}
150
+ onClick={() =>
151
+ change(
152
+ "tasks",
153
+ tasks.filter((_, i) => i !== index),
154
+ )
155
+ }
156
+ >
157
+ Remove draft
158
+ </button>
159
+ </fieldset>
160
+ ))}
161
+ {canAddTasks && tasks.length < 5 ? (
162
+ <button
163
+ type="button"
164
+ className="ph-btn ph-btn-secondary ph-btn-sm"
165
+ onClick={() =>
166
+ change("tasks", [
167
+ ...tasks,
168
+ {
169
+ title: "",
170
+ description: "",
171
+ priority: "low",
172
+ assign_me: false,
173
+ },
174
+ ])
175
+ }
176
+ >
177
+ Add task draft
178
+ </button>
179
+ ) : null}
180
+ </div>
181
+ ) : null}
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,179 @@
1
+ import {
2
+ cleanup,
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@testing-library/react";
8
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
9
+ import { SuiteAiPanel, type SuiteAiAction } from "./ai-panel";
10
+
11
+ beforeAll(() => {
12
+ // jsdom has no layout/scroll implementation; the real panel uses this API.
13
+ HTMLElement.prototype.scrollTo = vi.fn();
14
+ });
15
+ afterEach(cleanup);
16
+
17
+ function TestSpinner() {
18
+ return <span aria-hidden="true" />;
19
+ }
20
+
21
+ describe("integration draft review", () => {
22
+ it("shows the handover and source revisions without applying it", async () => {
23
+ const action: SuiteAiAction = {
24
+ id: "handover",
25
+ kind: "propose_project_handover",
26
+ status: "proposed",
27
+ summary: "Save a private project handover",
28
+ payload: {
29
+ body: "Open work and next steps from the brief.",
30
+ sources: [{ title: "Project brief", revision_number: 3 }],
31
+ },
32
+ };
33
+ const resolveAction = vi.fn();
34
+ const fetchChat = vi.fn().mockResolvedValue({
35
+ configured: true,
36
+ messages: [
37
+ { role: "assistant", content: "Review this draft.", actions: [action] },
38
+ ],
39
+ });
40
+ render(
41
+ <SuiteAiPanel
42
+ spinner={TestSpinner}
43
+ clearChat={vi.fn().mockResolvedValue(undefined)}
44
+ open
45
+ fetchChat={fetchChat}
46
+ sendMessage={vi.fn()}
47
+ resolveAction={resolveAction}
48
+ />,
49
+ );
50
+ fireEvent.click(
51
+ await screen.findByText("Review proposed content and evidence"),
52
+ );
53
+ expect(
54
+ screen.getByText("Open work and next steps from the brief."),
55
+ ).toBeVisible();
56
+ expect(screen.getByText(/"revision_number": 3/)).toBeVisible();
57
+ expect(resolveAction).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it("shows before/after content and delegates only explicit confirmation", async () => {
61
+ const action: SuiteAiAction = {
62
+ id: "requirements",
63
+ kind: "propose_requirement_update",
64
+ status: "proposed",
65
+ summary: "Update acceptance criteria",
66
+ payload: {
67
+ before: "Existing human note.",
68
+ description: "Existing human note. Added acceptance criterion.",
69
+ document_revision: 2,
70
+ task_updated_at: "2026-10-06T10:00:00Z",
71
+ },
72
+ };
73
+ const messages = [
74
+ {
75
+ role: "assistant" as const,
76
+ content: "Review the change.",
77
+ actions: [action],
78
+ },
79
+ ];
80
+ const resolveAction = vi.fn().mockResolvedValue({ messages });
81
+ render(
82
+ <SuiteAiPanel
83
+ spinner={TestSpinner}
84
+ clearChat={vi.fn().mockResolvedValue(undefined)}
85
+ open
86
+ fetchChat={vi.fn().mockResolvedValue({ configured: true, messages })}
87
+ sendMessage={vi.fn()}
88
+ resolveAction={resolveAction}
89
+ />,
90
+ );
91
+ fireEvent.click(
92
+ await screen.findByText("Review proposed content and evidence"),
93
+ );
94
+ expect(screen.getByText("Existing human note.")).toBeVisible();
95
+ expect(
96
+ screen.getByText("Existing human note. Added acceptance criterion."),
97
+ ).toBeVisible();
98
+ expect(resolveAction).not.toHaveBeenCalled();
99
+ fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
100
+ await waitFor(() =>
101
+ expect(resolveAction).toHaveBeenCalledWith("requirements", "confirm"),
102
+ );
103
+ });
104
+
105
+ it("keeps workflows available in an existing chat and only prefills the composer", async () => {
106
+ const sendMessage = vi.fn();
107
+ render(
108
+ <SuiteAiPanel
109
+ spinner={TestSpinner}
110
+ clearChat={vi.fn()}
111
+ open
112
+ fetchChat={vi
113
+ .fn()
114
+ .mockResolvedValue({
115
+ configured: true,
116
+ messages: [{ role: "assistant", content: "Existing conversation" }],
117
+ })}
118
+ sendMessage={sendMessage}
119
+ presets={[
120
+ {
121
+ label: "Prepare knowledge pack",
122
+ prompt: "Prepare a reviewed knowledge pack.",
123
+ },
124
+ ]}
125
+ />,
126
+ );
127
+ await screen.findByRole("button", { name: "Clear" });
128
+ fireEvent.click(screen.getByText("Workflows"));
129
+ fireEvent.click(
130
+ screen.getByRole("button", { name: "Prepare knowledge pack" }),
131
+ );
132
+ expect(
133
+ screen.getByDisplayValue("Prepare a reviewed knowledge pack."),
134
+ ).toBeVisible();
135
+ expect(sendMessage).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it("submits human-edited task drafts without exposing scope as an editable field", async () => {
139
+ const action: SuiteAiAction = {
140
+ id: "tasks",
141
+ kind: "propose_source_tasks",
142
+ status: "proposed",
143
+ summary: "Create source tasks",
144
+ payload: {
145
+ project_id: "project-1",
146
+ tasks: [{ title: "Initial task", description: "Criteria" }],
147
+ requested_by: "user-1",
148
+ },
149
+ };
150
+ const messages = [
151
+ {
152
+ role: "assistant" as const,
153
+ content: "Review task drafts.",
154
+ actions: [action],
155
+ },
156
+ ];
157
+ const resolveAction = vi.fn().mockResolvedValue({ messages });
158
+ render(
159
+ <SuiteAiPanel
160
+ spinner={TestSpinner}
161
+ clearChat={vi.fn()}
162
+ open
163
+ fetchChat={vi.fn().mockResolvedValue({ configured: true, messages })}
164
+ sendMessage={vi.fn()}
165
+ resolveAction={resolveAction}
166
+ />,
167
+ );
168
+ fireEvent.click(await screen.findByRole("button", { name: "Edit draft" }));
169
+ fireEvent.change(screen.getByLabelText("Title"), {
170
+ target: { value: "Human-edited task" },
171
+ });
172
+ fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
173
+ await waitFor(() =>
174
+ expect(resolveAction).toHaveBeenCalledWith("tasks", "confirm", {
175
+ tasks: [{ title: "Human-edited task", description: "Criteria" }],
176
+ }),
177
+ );
178
+ });
179
+ });