@useatlas/react 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +79 -2
- package/dist/{chunk-5SEVKHS5.cjs → chunk-35SCTKSW.js} +100 -7
- package/dist/chunk-35SCTKSW.js.map +1 -0
- package/dist/{chunk-UIRB6L36.cjs → chunk-DZFSZSQB.cjs} +46 -54
- package/dist/chunk-DZFSZSQB.cjs.map +1 -0
- package/dist/{chunk-2WFDP7G5.js → chunk-FMSGREKS.js} +46 -54
- package/dist/chunk-FMSGREKS.js.map +1 -0
- package/dist/{chunk-44HBZYKP.js → chunk-IDXGFWFS.cjs} +109 -3
- package/dist/chunk-IDXGFWFS.cjs.map +1 -0
- package/dist/global.d.ts +36 -0
- package/dist/hooks.cjs +10 -10
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +2 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +3 -3
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +385 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -4
- package/dist/index.d.ts +224 -4
- package/dist/index.js +328 -208
- package/dist/index.js.map +1 -1
- package/dist/lib/widget-types.d.ts +232 -0
- package/dist/{result-chart-YLCKBNV4.cjs → result-chart-ANZOT6FL.cjs} +24 -34
- package/dist/result-chart-ANZOT6FL.cjs.map +1 -0
- package/dist/{result-chart-NFAJ4IQ5.js → result-chart-C3EJTN5G.js} +22 -32
- package/dist/result-chart-C3EJTN5G.js.map +1 -0
- package/dist/widget.css +2 -2
- package/dist/widget.js +215 -246
- package/package.json +27 -17
- package/src/components/__tests__/data-table.test.tsx +125 -0
- package/src/components/actions/action-approval-card.tsx +26 -19
- package/src/components/actions/action-status-badge.tsx +3 -3
- package/src/components/atlas-chat.tsx +97 -37
- package/src/components/chart/result-chart.tsx +13 -37
- package/src/components/chat/api-key-bar.tsx +4 -4
- package/src/components/chat/data-table.tsx +42 -3
- package/src/components/chat/error-banner.tsx +108 -5
- package/src/components/chat/follow-up-chips.tsx +1 -1
- package/src/components/chat/managed-auth-card.tsx +6 -6
- package/src/components/conversations/conversation-item.tsx +19 -14
- package/src/components/conversations/conversation-list.tsx +3 -3
- package/src/components/conversations/conversation-sidebar.tsx +15 -4
- package/src/components/conversations/delete-confirmation.tsx +2 -2
- package/src/components/error-boundary.tsx +66 -0
- package/src/components/schema-explorer/schema-explorer.tsx +4 -0
- package/src/env.d.ts +9 -7
- package/src/global.d.ts +36 -0
- package/src/hooks/__tests__/use-atlas-conversations.test.tsx +4 -6
- package/src/hooks/use-atlas-chat.ts +1 -1
- package/src/hooks/use-atlas-conversations.ts +2 -2
- package/src/hooks/use-conversations.ts +60 -68
- package/src/index.ts +8 -0
- package/src/lib/action-types.ts +2 -2
- package/src/lib/helpers.ts +16 -16
- package/src/lib/types.ts +3 -2
- package/src/lib/widget-types.ts +232 -0
- package/src/test-setup.ts +2 -2
- package/dist/chunk-2WFDP7G5.js.map +0 -1
- package/dist/chunk-44HBZYKP.js.map +0 -1
- package/dist/chunk-5SEVKHS5.cjs.map +0 -1
- package/dist/chunk-UIRB6L36.cjs.map +0 -1
- package/dist/result-chart-NFAJ4IQ5.js.map +0 -1
- package/dist/result-chart-YLCKBNV4.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useatlas/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Embeddable Atlas chat UI for React applications",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/AtlasDevHQ/atlas",
|
|
9
|
+
"directory": "packages/react"
|
|
10
|
+
},
|
|
6
11
|
"type": "module",
|
|
7
12
|
"main": "./src/index.ts",
|
|
8
13
|
"module": "./src/index.ts",
|
|
@@ -10,6 +15,7 @@
|
|
|
10
15
|
"exports": {
|
|
11
16
|
".": "./src/index.ts",
|
|
12
17
|
"./hooks": "./src/hooks/index.ts",
|
|
18
|
+
"./widget": "./src/global.d.ts",
|
|
13
19
|
"./styles.css": "./src/styles.css"
|
|
14
20
|
},
|
|
15
21
|
"publishConfig": {
|
|
@@ -37,6 +43,9 @@
|
|
|
37
43
|
"default": "./dist/hooks.cjs"
|
|
38
44
|
}
|
|
39
45
|
},
|
|
46
|
+
"./widget": {
|
|
47
|
+
"types": "./dist/global.d.ts"
|
|
48
|
+
},
|
|
40
49
|
"./styles.css": "./dist/styles.css"
|
|
41
50
|
}
|
|
42
51
|
},
|
|
@@ -48,8 +57,9 @@
|
|
|
48
57
|
"*.css"
|
|
49
58
|
],
|
|
50
59
|
"scripts": {
|
|
51
|
-
"build": "tsup && cp src/styles.css dist/styles.css && bun x @tailwindcss/cli -i src/widget.css -o dist/widget.css --minify",
|
|
60
|
+
"build": "tsup && cp src/styles.css dist/styles.css && cp src/global.d.ts dist/global.d.ts && mkdir -p dist/lib && cp src/lib/widget-types.ts dist/lib/widget-types.d.ts && bun x @tailwindcss/cli -i src/widget.css -o dist/widget.css --minify",
|
|
52
61
|
"dev": "tsup --watch",
|
|
62
|
+
"test": "bun scripts/test-isolated.ts",
|
|
53
63
|
"type": "tsc --noEmit"
|
|
54
64
|
},
|
|
55
65
|
"peerDependencies": {
|
|
@@ -61,7 +71,7 @@
|
|
|
61
71
|
"react-syntax-highlighter": ">=15.0.0",
|
|
62
72
|
"recharts": ">=2.0.0",
|
|
63
73
|
"tailwindcss": ">=4.0.0",
|
|
64
|
-
"
|
|
74
|
+
"exceljs": ">=4.0.0"
|
|
65
75
|
},
|
|
66
76
|
"peerDependenciesMeta": {
|
|
67
77
|
"lucide-react": {
|
|
@@ -76,13 +86,13 @@
|
|
|
76
86
|
"tailwindcss": {
|
|
77
87
|
"optional": true
|
|
78
88
|
},
|
|
79
|
-
"
|
|
89
|
+
"exceljs": {
|
|
80
90
|
"optional": true
|
|
81
91
|
}
|
|
82
92
|
},
|
|
83
93
|
"dependencies": {
|
|
84
|
-
"@useatlas/types": "
|
|
85
|
-
"@radix-ui/react-slot": "^1.2.
|
|
94
|
+
"@useatlas/types": "^0.0.2",
|
|
95
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
86
96
|
"class-variance-authority": "^0.7.1",
|
|
87
97
|
"clsx": "^2.1.1",
|
|
88
98
|
"radix-ui": "^1.4.3",
|
|
@@ -91,23 +101,23 @@
|
|
|
91
101
|
"tailwind-merge": "^3.5.0"
|
|
92
102
|
},
|
|
93
103
|
"devDependencies": {
|
|
94
|
-
"@ai-sdk/react": "^3.0.
|
|
95
|
-
"@tailwindcss/cli": "^4.2.
|
|
104
|
+
"@ai-sdk/react": "^3.0.143",
|
|
105
|
+
"@tailwindcss/cli": "^4.2.2",
|
|
96
106
|
"@testing-library/dom": "^10.4.1",
|
|
97
107
|
"@testing-library/react": "^16.3.2",
|
|
98
108
|
"@types/react": "^19.2.14",
|
|
99
109
|
"@types/react-dom": "^19.2.3",
|
|
100
110
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
101
|
-
"ai": "^6.0.
|
|
102
|
-
"
|
|
103
|
-
"
|
|
111
|
+
"ai": "^6.0.141",
|
|
112
|
+
"exceljs": "^4.4.0",
|
|
113
|
+
"happy-dom": "20.8.9",
|
|
114
|
+
"lucide-react": "^1.7.0",
|
|
104
115
|
"react": "^19.2.4",
|
|
105
116
|
"react-dom": "^19.2.4",
|
|
106
|
-
"react-syntax-highlighter": "^16.1.
|
|
107
|
-
"recharts": "^3.
|
|
108
|
-
"tailwindcss": "^4.2.
|
|
109
|
-
"tsup": "^8.5.
|
|
110
|
-
"typescript": "^
|
|
111
|
-
"xlsx": "^0.18.5"
|
|
117
|
+
"react-syntax-highlighter": "^16.1.1",
|
|
118
|
+
"recharts": "^3.8.1",
|
|
119
|
+
"tailwindcss": "^4.2.2",
|
|
120
|
+
"tsup": "^8.5.1",
|
|
121
|
+
"typescript": "^6.0.2"
|
|
112
122
|
}
|
|
113
123
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { render, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { DataTable } from "../chat/data-table";
|
|
4
|
+
|
|
5
|
+
describe("DataTable", () => {
|
|
6
|
+
const columns = ["name", "revenue", "city"];
|
|
7
|
+
const rows = [
|
|
8
|
+
{ name: "Acme", revenue: 500000, city: "NYC" },
|
|
9
|
+
{ name: "Beta", revenue: 300000, city: "LA" },
|
|
10
|
+
{ name: "Gamma", revenue: 200000, city: "SF" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
test("renders column headers", () => {
|
|
14
|
+
const { container } = render(<DataTable columns={columns} rows={rows} />);
|
|
15
|
+
const ths = container.querySelectorAll("th");
|
|
16
|
+
const headers = Array.from(ths).map((th) => th.textContent?.trim());
|
|
17
|
+
expect(headers).toEqual(["name", "revenue", "city"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("renders correct number of data rows", () => {
|
|
21
|
+
const { container } = render(<DataTable columns={columns} rows={rows} />);
|
|
22
|
+
const trs = container.querySelectorAll("tbody tr");
|
|
23
|
+
expect(trs.length).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("renders cell values", () => {
|
|
27
|
+
const { container } = render(<DataTable columns={columns} rows={rows} />);
|
|
28
|
+
expect(container.textContent).toContain("Acme");
|
|
29
|
+
expect(container.textContent).toContain("NYC");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("formats numeric values with locale separators", () => {
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<DataTable columns={["val"]} rows={[{ val: 1234567 }]} />,
|
|
35
|
+
);
|
|
36
|
+
const text = container.textContent!;
|
|
37
|
+
// formatCell uses toLocaleString() — verify formatted output contains separators
|
|
38
|
+
// (exact format varies by locale, but should match X,XXX,XXX or X.XXX.XXX pattern)
|
|
39
|
+
expect(text).toMatch(/1[,.\s]234[,.\s]567/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("renders em-dash for null values", () => {
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<DataTable columns={["val"]} rows={[{ val: null }]} />,
|
|
45
|
+
);
|
|
46
|
+
expect(container.textContent).toContain("\u2014");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("truncates to maxRows and shows overflow message", () => {
|
|
50
|
+
const manyRows = Array.from({ length: 25 }, (_, i) => ({ name: `Item ${i}` }));
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<DataTable columns={["name"]} rows={manyRows} maxRows={10} />,
|
|
53
|
+
);
|
|
54
|
+
const trs = container.querySelectorAll("tbody tr");
|
|
55
|
+
expect(trs.length).toBe(10);
|
|
56
|
+
expect(container.textContent).toContain("Showing 10 of 25 rows");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("no overflow message when rows fit", () => {
|
|
60
|
+
const { container } = render(<DataTable columns={columns} rows={rows} maxRows={10} />);
|
|
61
|
+
expect(container.textContent).not.toContain("Showing");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("sort ascending on column click", () => {
|
|
65
|
+
const { container } = render(<DataTable columns={columns} rows={rows} />);
|
|
66
|
+
const ths = container.querySelectorAll("th");
|
|
67
|
+
const nameHeader = ths[0];
|
|
68
|
+
|
|
69
|
+
// Click to sort ascending by name
|
|
70
|
+
fireEvent.click(nameHeader);
|
|
71
|
+
|
|
72
|
+
const firstCell = container.querySelector("tbody tr td");
|
|
73
|
+
expect(firstCell!.textContent).toBe("Acme");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("sort descending on second click", () => {
|
|
77
|
+
const { container } = render(<DataTable columns={columns} rows={rows} />);
|
|
78
|
+
const ths = container.querySelectorAll("th");
|
|
79
|
+
const nameHeader = ths[0];
|
|
80
|
+
|
|
81
|
+
fireEvent.click(nameHeader); // asc
|
|
82
|
+
fireEvent.click(nameHeader); // desc
|
|
83
|
+
|
|
84
|
+
const cells = container.querySelectorAll("tbody tr td:first-child");
|
|
85
|
+
expect(cells[0].textContent).toBe("Gamma");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("third click resets sort to original order", () => {
|
|
89
|
+
// Use data where original order differs from both asc and desc
|
|
90
|
+
const unsortedRows = [
|
|
91
|
+
{ name: "Beta", revenue: 300000, city: "LA" },
|
|
92
|
+
{ name: "Acme", revenue: 500000, city: "NYC" },
|
|
93
|
+
{ name: "Gamma", revenue: 200000, city: "SF" },
|
|
94
|
+
];
|
|
95
|
+
const { container } = render(<DataTable columns={columns} rows={unsortedRows} />);
|
|
96
|
+
const ths = container.querySelectorAll("th");
|
|
97
|
+
const nameHeader = ths[0];
|
|
98
|
+
|
|
99
|
+
fireEvent.click(nameHeader); // asc: Acme, Beta, Gamma
|
|
100
|
+
fireEvent.click(nameHeader); // desc: Gamma, Beta, Acme
|
|
101
|
+
fireEvent.click(nameHeader); // reset: Beta, Acme, Gamma (original)
|
|
102
|
+
|
|
103
|
+
const firstCell = container.querySelector("tbody tr td");
|
|
104
|
+
expect(firstCell!.textContent).toBe("Beta");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("renders with array rows (unknown[][])", () => {
|
|
108
|
+
const arrayRows = [
|
|
109
|
+
["Alice", 100],
|
|
110
|
+
["Bob", 200],
|
|
111
|
+
];
|
|
112
|
+
const { container } = render(
|
|
113
|
+
<DataTable columns={["name", "score"]} rows={arrayRows} />,
|
|
114
|
+
);
|
|
115
|
+
expect(container.textContent).toContain("Alice");
|
|
116
|
+
expect(container.textContent).toContain("Bob");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("renders empty state message when rows are empty", () => {
|
|
120
|
+
const { container } = render(<DataTable columns={["id"]} rows={[]} />);
|
|
121
|
+
expect(container.querySelector("table")).toBeNull();
|
|
122
|
+
expect(container.textContent).toContain("Query returned no results");
|
|
123
|
+
expect(container.textContent).toContain("Try adjusting your query filters or criteria");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -5,8 +5,8 @@ import { getToolArgs, getToolResult, isToolComplete } from "../../lib/helpers";
|
|
|
5
5
|
import {
|
|
6
6
|
isActionToolResult,
|
|
7
7
|
RESOLVED_STATUSES,
|
|
8
|
-
type
|
|
9
|
-
type
|
|
8
|
+
type ActionDisplayStatus,
|
|
9
|
+
type ResolvedDisplayStatus,
|
|
10
10
|
type ActionApprovalResponse,
|
|
11
11
|
type ActionToolResultShape,
|
|
12
12
|
} from "../../lib/action-types";
|
|
@@ -35,14 +35,14 @@ function safeStringify(value: unknown): string {
|
|
|
35
35
|
type CardState =
|
|
36
36
|
| { phase: "idle" }
|
|
37
37
|
| { phase: "submitting"; action: "approve" | "deny" }
|
|
38
|
-
| { phase: "resolved"; status:
|
|
38
|
+
| { phase: "resolved"; status: ResolvedDisplayStatus; result?: unknown }
|
|
39
39
|
| { phase: "error"; message: string };
|
|
40
40
|
|
|
41
41
|
/* ------------------------------------------------------------------ */
|
|
42
42
|
/* Border color by status */
|
|
43
43
|
/* ------------------------------------------------------------------ */
|
|
44
44
|
|
|
45
|
-
function borderColor(status:
|
|
45
|
+
function borderColor(status: ActionDisplayStatus): string {
|
|
46
46
|
switch (status) {
|
|
47
47
|
case "pending_approval":
|
|
48
48
|
return "border-yellow-300 dark:border-yellow-900/50";
|
|
@@ -53,7 +53,8 @@ function borderColor(status: ActionStatus): string {
|
|
|
53
53
|
case "denied":
|
|
54
54
|
case "failed":
|
|
55
55
|
return "border-red-300 dark:border-red-900/50";
|
|
56
|
-
|
|
56
|
+
case "rolled_back":
|
|
57
|
+
case "timed_out":
|
|
57
58
|
return "border-zinc-200 dark:border-zinc-700";
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -87,12 +88,16 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
87
88
|
const toolResult: ActionToolResultShape = rawResult;
|
|
88
89
|
|
|
89
90
|
// Effective status: local optimistic update wins over server result
|
|
90
|
-
const effectiveStatus:
|
|
91
|
+
const effectiveStatus: ActionDisplayStatus =
|
|
91
92
|
cardState.phase === "resolved" ? cardState.status : toolResult.status;
|
|
92
93
|
|
|
93
94
|
const isPending = effectiveStatus === "pending_approval" && cardState.phase !== "submitting";
|
|
94
95
|
const isSubmitting = cardState.phase === "submitting";
|
|
95
|
-
const resolvedResult = cardState.phase === "resolved"
|
|
96
|
+
const resolvedResult = cardState.phase === "resolved"
|
|
97
|
+
? cardState.result
|
|
98
|
+
: toolResult.status === "approved" || toolResult.status === "executed" || toolResult.status === "auto_approved"
|
|
99
|
+
? toolResult.result
|
|
100
|
+
: undefined;
|
|
96
101
|
|
|
97
102
|
/* ---------------------------------------------------------------- */
|
|
98
103
|
/* API helpers */
|
|
@@ -127,8 +132,10 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
127
132
|
} catch {
|
|
128
133
|
throw new Error("Action was already resolved, but the response could not be read. Refresh the page.");
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
if (typeof data.status !== "string" || !RESOLVED_STATUSES.has(data.status as ActionDisplayStatus)) {
|
|
136
|
+
throw new Error("Action was already resolved with an unrecognized status. Refresh the page.");
|
|
137
|
+
}
|
|
138
|
+
const label = data.status.replace(/_/g, " ");
|
|
132
139
|
throw new Error(`This action was already ${label} by another user or policy.`);
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -143,10 +150,10 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
143
150
|
} catch {
|
|
144
151
|
throw new Error("Action succeeded, but the response could not be read. Refresh the page.");
|
|
145
152
|
}
|
|
146
|
-
if (typeof data.status !== "string") {
|
|
147
|
-
throw new Error("Action succeeded, but the server returned an
|
|
153
|
+
if (typeof data.status !== "string" || !RESOLVED_STATUSES.has(data.status as ActionDisplayStatus)) {
|
|
154
|
+
throw new Error("Action succeeded, but the server returned an unrecognized status. Refresh the page.");
|
|
148
155
|
}
|
|
149
|
-
setCardState({ phase: "resolved", status: data.status as
|
|
156
|
+
setCardState({ phase: "resolved", status: data.status as ResolvedDisplayStatus, result: data.result });
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
async function handleApprove() {
|
|
@@ -215,13 +222,13 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
215
222
|
: safeStringify(resolvedResult)}
|
|
216
223
|
</div>
|
|
217
224
|
)}
|
|
218
|
-
{toolResult.
|
|
225
|
+
{toolResult.status === "failed" && (
|
|
219
226
|
<div className="mb-2 rounded bg-red-50 p-2 text-xs text-red-700 dark:bg-red-900/20 dark:text-red-400">
|
|
220
227
|
<span className="font-medium">Error: </span>
|
|
221
228
|
{toolResult.error}
|
|
222
229
|
</div>
|
|
223
230
|
)}
|
|
224
|
-
{toolResult.
|
|
231
|
+
{toolResult.status === "denied" && RESOLVED_STATUSES.has(effectiveStatus) && (
|
|
225
232
|
<div className="mb-2 text-xs text-zinc-500 dark:text-zinc-400">
|
|
226
233
|
<span className="font-medium">Reason: </span>
|
|
227
234
|
{toolResult.reason}
|
|
@@ -241,7 +248,7 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
241
248
|
<button
|
|
242
249
|
onClick={handleApprove}
|
|
243
250
|
disabled={isSubmitting}
|
|
244
|
-
className="inline-flex items-center gap-1.5 rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40"
|
|
251
|
+
className="inline-flex items-center gap-1.5 rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 disabled:opacity-40"
|
|
245
252
|
>
|
|
246
253
|
{isSubmitting && cardState.action === "approve" && (
|
|
247
254
|
<span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
@@ -253,7 +260,7 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
253
260
|
<button
|
|
254
261
|
onClick={() => setShowDenyInput(true)}
|
|
255
262
|
disabled={isSubmitting}
|
|
256
|
-
className="rounded border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-800 disabled:opacity-40 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
|
|
263
|
+
className="rounded border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
|
|
257
264
|
>
|
|
258
265
|
Deny
|
|
259
266
|
</button>
|
|
@@ -263,13 +270,13 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
263
270
|
value={denyReason}
|
|
264
271
|
onChange={(e) => setDenyReason(e.target.value)}
|
|
265
272
|
placeholder="Reason (optional)"
|
|
266
|
-
className="flex-1 rounded border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 placeholder-zinc-400 outline-none focus:border-red-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
|
|
273
|
+
className="flex-1 rounded border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-red-400 focus-visible:ring-[3px] focus-visible:ring-red-400/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
|
|
267
274
|
disabled={isSubmitting}
|
|
268
275
|
/>
|
|
269
276
|
<button
|
|
270
277
|
onClick={handleDeny}
|
|
271
278
|
disabled={isSubmitting}
|
|
272
|
-
className="inline-flex items-center gap-1.5 rounded bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-500 disabled:opacity-40"
|
|
279
|
+
className="inline-flex items-center gap-1.5 rounded bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-red-500/50 disabled:opacity-40"
|
|
273
280
|
>
|
|
274
281
|
{isSubmitting && cardState.action === "deny" && (
|
|
275
282
|
<span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
@@ -282,7 +289,7 @@ export function ActionApprovalCard({ part }: { part: unknown }) {
|
|
|
282
289
|
setDenyReason("");
|
|
283
290
|
}}
|
|
284
291
|
disabled={isSubmitting}
|
|
285
|
-
className="text-xs text-zinc-400 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300"
|
|
292
|
+
className="rounded text-xs text-zinc-400 hover:text-zinc-600 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40 dark:hover:text-zinc-300"
|
|
286
293
|
>
|
|
287
294
|
Cancel
|
|
288
295
|
</button>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ActionDisplayStatus } from "../../lib/action-types";
|
|
4
4
|
|
|
5
|
-
const STATUS_CONFIG: Record<
|
|
5
|
+
const STATUS_CONFIG: Record<ActionDisplayStatus, { label: string; classes: string }> = {
|
|
6
6
|
pending_approval: {
|
|
7
7
|
label: "Pending Approval",
|
|
8
8
|
classes: "bg-yellow-100 text-yellow-700 dark:bg-yellow-600/20 dark:text-yellow-400",
|
|
@@ -37,7 +37,7 @@ const STATUS_CONFIG: Record<ActionStatus, { label: string; classes: string }> =
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export function ActionStatusBadge({ status }: { status:
|
|
40
|
+
export function ActionStatusBadge({ status }: { status: ActionDisplayStatus }) {
|
|
41
41
|
const config = STATUS_CONFIG[status] ?? {
|
|
42
42
|
label: status.replace(/_/g, " "),
|
|
43
43
|
classes: "bg-zinc-100 text-zinc-700 dark:bg-zinc-600/20 dark:text-zinc-400",
|