@dowel-ui/react 0.1.1 → 0.3.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/LICENSE +1 -1
- package/README.md +193 -0
- package/dist/components/ai-action-ledger/ai-action-ledger.d.ts +77 -0
- package/dist/components/ai-action-ledger/ai-action-ledger.d.ts.map +1 -0
- package/dist/components/ai-action-ledger/ai-action-ledger.js +224 -0
- package/dist/components/ai-action-ledger/ai-action-ledger.js.map +1 -0
- package/dist/components/ai-action-ledger/index.d.ts +2 -0
- package/dist/components/ai-action-ledger/index.js +2 -0
- package/dist/components/ai-action-ledger/meta.js +17 -0
- package/dist/components/ai-action-ledger/meta.js.map +1 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts +50 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts.map +1 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js +156 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -0
- package/dist/components/ai-agent-plan/index.d.ts +2 -0
- package/dist/components/ai-agent-plan/index.js +2 -0
- package/dist/components/ai-agent-plan/meta.js +17 -0
- package/dist/components/ai-agent-plan/meta.js.map +1 -0
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-approval-request/ai-approval-request.d.ts +67 -0
- package/dist/components/ai-approval-request/ai-approval-request.d.ts.map +1 -0
- package/dist/components/ai-approval-request/ai-approval-request.js +218 -0
- package/dist/components/ai-approval-request/ai-approval-request.js.map +1 -0
- package/dist/components/ai-approval-request/index.d.ts +2 -0
- package/dist/components/ai-approval-request/index.js +2 -0
- package/dist/components/ai-approval-request/meta.js +17 -0
- package/dist/components/ai-approval-request/meta.js.map +1 -0
- package/dist/components/ai-inline-completion/ai-inline-completion.d.ts +45 -0
- package/dist/components/ai-inline-completion/ai-inline-completion.d.ts.map +1 -0
- package/dist/components/ai-inline-completion/ai-inline-completion.js +110 -0
- package/dist/components/ai-inline-completion/ai-inline-completion.js.map +1 -0
- package/dist/components/ai-inline-completion/index.d.ts +2 -0
- package/dist/components/ai-inline-completion/index.js +2 -0
- package/dist/components/ai-inline-completion/meta.js +17 -0
- package/dist/components/ai-inline-completion/meta.js.map +1 -0
- package/dist/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/components/ai-response/ai-response.js +1 -0
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.d.ts +75 -0
- package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -0
- package/dist/components/ai-structured-output/ai-structured-output.js +137 -0
- package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -0
- package/dist/components/ai-structured-output/index.d.ts +2 -0
- package/dist/components/ai-structured-output/index.js +2 -0
- package/dist/components/ai-structured-output/meta.js +17 -0
- package/dist/components/ai-structured-output/meta.js.map +1 -0
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/file-upload/file-upload.d.ts +46 -0
- package/dist/components/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload/file-upload.js +204 -0
- package/dist/components/file-upload/file-upload.js.map +1 -0
- package/dist/components/file-upload/index.d.ts +3 -0
- package/dist/components/file-upload/index.js +3 -0
- package/dist/components/file-upload/meta.js +17 -0
- package/dist/components/file-upload/meta.js.map +1 -0
- package/dist/components/file-upload/upload-queue.d.ts +87 -0
- package/dist/components/file-upload/upload-queue.d.ts.map +1 -0
- package/dist/components/file-upload/upload-queue.js +249 -0
- package/dist/components/file-upload/upload-queue.js.map +1 -0
- package/dist/components/meter/index.d.ts +2 -0
- package/dist/components/meter/index.js +2 -0
- package/dist/components/meter/meta.js +17 -0
- package/dist/components/meter/meta.js.map +1 -0
- package/dist/components/meter/meter.d.ts +69 -0
- package/dist/components/meter/meter.d.ts.map +1 -0
- package/dist/components/meter/meter.js +144 -0
- package/dist/components/meter/meter.js.map +1 -0
- package/dist/components/metric-delta/index.d.ts +2 -0
- package/dist/components/metric-delta/index.js +2 -0
- package/dist/components/metric-delta/meta.js +17 -0
- package/dist/components/metric-delta/meta.js.map +1 -0
- package/dist/components/metric-delta/metric-delta.d.ts +59 -0
- package/dist/components/metric-delta/metric-delta.d.ts.map +1 -0
- package/dist/components/metric-delta/metric-delta.js +82 -0
- package/dist/components/metric-delta/metric-delta.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/progress/progress.js +1 -0
- package/dist/components/progress/progress.js.map +1 -1
- package/dist/components/record-diff/index.d.ts +2 -0
- package/dist/components/record-diff/index.js +2 -0
- package/dist/components/record-diff/meta.js +17 -0
- package/dist/components/record-diff/meta.js.map +1 -0
- package/dist/components/record-diff/record-diff.d.ts +56 -0
- package/dist/components/record-diff/record-diff.d.ts.map +1 -0
- package/dist/components/record-diff/record-diff.js +199 -0
- package/dist/components/record-diff/record-diff.js.map +1 -0
- package/dist/components/spinner/spinner.js +1 -0
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/tags-input/index.d.ts +2 -0
- package/dist/components/tags-input/index.js +2 -0
- package/dist/components/tags-input/meta.js +17 -0
- package/dist/components/tags-input/meta.js.map +1 -0
- package/dist/components/tags-input/tags-input.d.ts +47 -0
- package/dist/components/tags-input/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input/tags-input.js +182 -0
- package/dist/components/tags-input/tags-input.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +12 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +65 -45
- package/dist/registry/components.js.map +1 -1
- package/package.json +36 -4
- package/src/components/ai-action-ledger/ai-action-ledger.tsx +381 -0
- package/src/components/ai-action-ledger/index.ts +14 -0
- package/src/components/ai-action-ledger/meta.ts +19 -0
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-agent-plan/index.ts +10 -0
- package/src/components/ai-agent-plan/meta.ts +19 -0
- package/src/components/ai-approval-request/ai-approval-request.tsx +368 -0
- package/src/components/ai-approval-request/index.ts +7 -0
- package/src/components/ai-approval-request/meta.ts +21 -0
- package/src/components/ai-inline-completion/ai-inline-completion.tsx +214 -0
- package/src/components/ai-inline-completion/index.ts +1 -0
- package/src/components/ai-inline-completion/meta.ts +20 -0
- package/src/components/ai-response/ai-response.tsx +1 -0
- package/src/components/ai-structured-output/ai-structured-output.tsx +268 -0
- package/src/components/ai-structured-output/index.ts +10 -0
- package/src/components/ai-structured-output/meta.ts +18 -0
- package/src/components/file-upload/file-upload.tsx +322 -0
- package/src/components/file-upload/index.ts +20 -0
- package/src/components/file-upload/meta.ts +21 -0
- package/src/components/file-upload/upload-queue.ts +337 -0
- package/src/components/meter/index.ts +8 -0
- package/src/components/meter/meta.ts +20 -0
- package/src/components/meter/meter.tsx +219 -0
- package/src/components/metric-delta/index.ts +6 -0
- package/src/components/metric-delta/meta.ts +18 -0
- package/src/components/metric-delta/metric-delta.tsx +180 -0
- package/src/components/progress/progress.tsx +3 -0
- package/src/components/record-diff/index.ts +10 -0
- package/src/components/record-diff/meta.ts +19 -0
- package/src/components/record-diff/record-diff.tsx +341 -0
- package/src/components/spinner/spinner.tsx +5 -0
- package/src/components/tags-input/index.ts +1 -0
- package/src/components/tags-input/meta.ts +23 -0
- package/src/components/tags-input/tags-input.tsx +300 -0
- package/src/index.ts +10 -0
- package/src/registry/components.ts +20 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
type ComponentPropsWithRef,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from "react";
|
|
14
|
+
|
|
15
|
+
import { focusRing } from "@/lib/styles";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What an agent actually did, and what can be undone.
|
|
20
|
+
*
|
|
21
|
+
* Every AI component library stops at the conversation boundary: approve a tool
|
|
22
|
+
* call before it runs, then treat whatever it did to the database as the
|
|
23
|
+
* application's problem. But the thing that blocks agents from getting write
|
|
24
|
+
* access in real products is not "did I approve this", it is "it did the wrong
|
|
25
|
+
* thing, now what".
|
|
26
|
+
*
|
|
27
|
+
* The load-bearing idea is that undo is not uniform. Deleting a row can be
|
|
28
|
+
* reverted. A refund can be compensated by another transaction, which is not
|
|
29
|
+
* the same as never having happened. A sent email cannot be taken back at all.
|
|
30
|
+
* Presenting all three behind one "Undo" button is a lie the user only
|
|
31
|
+
* discovers after clicking, so reversibility is a required field and the
|
|
32
|
+
* affordance follows from it.
|
|
33
|
+
*
|
|
34
|
+
* This renders and selects. It performs nothing — `onRevert` receives the
|
|
35
|
+
* chosen actions and the application decides what that means.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export type Reversibility = "revertible" | "compensable" | "irreversible";
|
|
39
|
+
|
|
40
|
+
export type ActionStatus = "applied" | "reverting" | "reverted" | "failed";
|
|
41
|
+
|
|
42
|
+
export interface LedgerAction {
|
|
43
|
+
id: string;
|
|
44
|
+
/** What was done, in the user's language: "Deleted 3 contacts". */
|
|
45
|
+
summary: string;
|
|
46
|
+
reversibility: Reversibility;
|
|
47
|
+
status: ActionStatus;
|
|
48
|
+
/** What the action touched, for grouping and scanning. */
|
|
49
|
+
target?: string;
|
|
50
|
+
timestamp?: string;
|
|
51
|
+
/** Why a revert failed. Shown only when status is "failed". */
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const REVERSIBILITY_LABEL: Record<Reversibility, string> = {
|
|
56
|
+
revertible: "Can be undone",
|
|
57
|
+
compensable: "Can be offset, not undone",
|
|
58
|
+
irreversible: "Cannot be undone",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const STATUS_LABEL: Record<ActionStatus, string> = {
|
|
62
|
+
applied: "Applied",
|
|
63
|
+
reverting: "Reverting",
|
|
64
|
+
reverted: "Reverted",
|
|
65
|
+
failed: "Revert failed",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Only an applied action can be selected — the rest are already resolved. */
|
|
69
|
+
function isSelectable(action: LedgerAction): boolean {
|
|
70
|
+
return action.status === "applied" && action.reversibility !== "irreversible";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ledgerEntryVariants = cva("rounded-lg border text-sm transition-colors", {
|
|
74
|
+
variants: {
|
|
75
|
+
status: {
|
|
76
|
+
applied: "border-border bg-card",
|
|
77
|
+
reverting: "border-info/30 bg-info/5",
|
|
78
|
+
reverted: "border-border bg-muted/40",
|
|
79
|
+
failed: "border-destructive/40 bg-destructive/5",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultVariants: { status: "applied" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
interface LedgerContextValue {
|
|
86
|
+
actions: LedgerAction[];
|
|
87
|
+
selected: Set<string>;
|
|
88
|
+
toggle: (id: string) => void;
|
|
89
|
+
selectAll: () => void;
|
|
90
|
+
clear: () => void;
|
|
91
|
+
selectableIds: string[];
|
|
92
|
+
onRevert?: (actions: LedgerAction[]) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LedgerContext = createContext<LedgerContextValue | null>(null);
|
|
96
|
+
|
|
97
|
+
function useLedgerContext(component: string): LedgerContextValue {
|
|
98
|
+
const context = useContext(LedgerContext);
|
|
99
|
+
if (!context) {
|
|
100
|
+
throw new Error(`${component} must be rendered inside <ActionLedger>.`);
|
|
101
|
+
}
|
|
102
|
+
return context;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ActionLedgerProps extends Omit<ComponentPropsWithRef<"div">, "onSelect"> {
|
|
106
|
+
actions: LedgerAction[];
|
|
107
|
+
/** Receives the selected actions. The application performs the revert. */
|
|
108
|
+
onRevert?: (actions: LedgerAction[]) => void;
|
|
109
|
+
children?: ReactNode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function ActionLedger({
|
|
113
|
+
className,
|
|
114
|
+
actions,
|
|
115
|
+
onRevert,
|
|
116
|
+
children,
|
|
117
|
+
...props
|
|
118
|
+
}: ActionLedgerProps) {
|
|
119
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
120
|
+
|
|
121
|
+
const selectableIds = useMemo(
|
|
122
|
+
() => actions.filter(isSelectable).map((action) => action.id),
|
|
123
|
+
[actions],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const context = useMemo<LedgerContextValue>(
|
|
127
|
+
() => ({
|
|
128
|
+
actions,
|
|
129
|
+
selected,
|
|
130
|
+
selectableIds,
|
|
131
|
+
onRevert,
|
|
132
|
+
toggle: (id: string) => {
|
|
133
|
+
setSelected((current) => {
|
|
134
|
+
const next = new Set(current);
|
|
135
|
+
if (next.has(id)) next.delete(id);
|
|
136
|
+
else next.add(id);
|
|
137
|
+
return next;
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
selectAll: () => {
|
|
141
|
+
setSelected(new Set(selectableIds));
|
|
142
|
+
},
|
|
143
|
+
clear: () => {
|
|
144
|
+
setSelected(new Set());
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
[actions, selected, selectableIds, onRevert],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<LedgerContext.Provider value={context}>
|
|
152
|
+
<div
|
|
153
|
+
data-slot="action-ledger"
|
|
154
|
+
className={cn("flex flex-col gap-3", className)}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</div>
|
|
159
|
+
</LedgerContext.Provider>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Selection controls and the revert action.
|
|
165
|
+
*
|
|
166
|
+
* The count is stated on the button rather than beside it, so the accessible
|
|
167
|
+
* name says what pressing it will do.
|
|
168
|
+
*/
|
|
169
|
+
export function ActionLedgerToolbar({ className, ...props }: ComponentPropsWithRef<"div">) {
|
|
170
|
+
const { selected, selectableIds, actions, selectAll, clear, onRevert } =
|
|
171
|
+
useLedgerContext("ActionLedgerToolbar");
|
|
172
|
+
|
|
173
|
+
const count = selected.size;
|
|
174
|
+
const allSelected = selectableIds.length > 0 && count === selectableIds.length;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
data-slot="action-ledger-toolbar"
|
|
179
|
+
className={cn("flex flex-wrap items-center gap-2", className)}
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
disabled={selectableIds.length === 0}
|
|
185
|
+
onClick={allSelected ? clear : selectAll}
|
|
186
|
+
className={cn(
|
|
187
|
+
"rounded-md border border-input bg-background px-2.5 py-1 text-xs font-medium",
|
|
188
|
+
"transition-colors hover:bg-accent hover:text-accent-foreground",
|
|
189
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
190
|
+
focusRing,
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
{allSelected
|
|
194
|
+
? "Clear selection"
|
|
195
|
+
: `Select all ${String(selectableIds.length)} undoable`}
|
|
196
|
+
</button>
|
|
197
|
+
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
data-slot="action-ledger-revert"
|
|
201
|
+
disabled={count === 0}
|
|
202
|
+
onClick={() => {
|
|
203
|
+
onRevert?.(actions.filter((action) => selected.has(action.id)));
|
|
204
|
+
}}
|
|
205
|
+
className={cn(
|
|
206
|
+
"rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground",
|
|
207
|
+
"transition-colors hover:bg-primary-hover",
|
|
208
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
209
|
+
focusRing,
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
{count === 0 ? "Undo selected" : `Undo ${String(count)} selected`}
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ActionLedgerEntryProps
|
|
219
|
+
extends
|
|
220
|
+
Omit<ComponentPropsWithRef<"li">, "children">,
|
|
221
|
+
VariantProps<typeof ledgerEntryVariants> {
|
|
222
|
+
action: LedgerAction;
|
|
223
|
+
/** The payload inspector. Collapsed, because it is provenance not content. */
|
|
224
|
+
children?: ReactNode;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function ActionLedgerEntry({
|
|
228
|
+
className,
|
|
229
|
+
action,
|
|
230
|
+
children,
|
|
231
|
+
...props
|
|
232
|
+
}: ActionLedgerEntryProps) {
|
|
233
|
+
const { selected, toggle } = useLedgerContext("ActionLedgerEntry");
|
|
234
|
+
const checkboxId = useId();
|
|
235
|
+
const selectable = isSelectable(action);
|
|
236
|
+
const isSelected = selected.has(action.id);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<li
|
|
240
|
+
data-slot="action-ledger-entry"
|
|
241
|
+
data-status={action.status}
|
|
242
|
+
data-reversibility={action.reversibility}
|
|
243
|
+
className={cn(ledgerEntryVariants({ status: action.status }), className)}
|
|
244
|
+
{...props}
|
|
245
|
+
>
|
|
246
|
+
<div className="flex items-start gap-3 p-3">
|
|
247
|
+
{/* A native checkbox: this is a list of things to act on, and the
|
|
248
|
+
selection has to survive without JavaScript-driven roles. */}
|
|
249
|
+
<input
|
|
250
|
+
type="checkbox"
|
|
251
|
+
id={checkboxId}
|
|
252
|
+
disabled={!selectable}
|
|
253
|
+
checked={isSelected}
|
|
254
|
+
onChange={() => {
|
|
255
|
+
toggle(action.id);
|
|
256
|
+
}}
|
|
257
|
+
className={cn(
|
|
258
|
+
"mt-0.5 size-4 shrink-0 rounded border-input",
|
|
259
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
260
|
+
focusRing,
|
|
261
|
+
)}
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
265
|
+
<label
|
|
266
|
+
htmlFor={checkboxId}
|
|
267
|
+
className={cn("font-medium", !selectable && "opacity-70")}
|
|
268
|
+
>
|
|
269
|
+
{action.summary}
|
|
270
|
+
</label>
|
|
271
|
+
|
|
272
|
+
<p className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
|
273
|
+
{/* Reversibility in words, always. It decides whether the undo
|
|
274
|
+
button means anything, so it cannot be a colour or an icon. */}
|
|
275
|
+
<span data-slot="action-ledger-reversibility">
|
|
276
|
+
{REVERSIBILITY_LABEL[action.reversibility]}
|
|
277
|
+
</span>
|
|
278
|
+
<span aria-hidden="true">·</span>
|
|
279
|
+
<span data-slot="action-ledger-status">{STATUS_LABEL[action.status]}</span>
|
|
280
|
+
{action.target ? (
|
|
281
|
+
<>
|
|
282
|
+
<span aria-hidden="true">·</span>
|
|
283
|
+
<span>{action.target}</span>
|
|
284
|
+
</>
|
|
285
|
+
) : null}
|
|
286
|
+
{action.timestamp ? (
|
|
287
|
+
<>
|
|
288
|
+
<span aria-hidden="true">·</span>
|
|
289
|
+
<time dateTime={action.timestamp}>{action.timestamp}</time>
|
|
290
|
+
</>
|
|
291
|
+
) : null}
|
|
292
|
+
</p>
|
|
293
|
+
|
|
294
|
+
{/* A failed revert is the state people most need to understand, so
|
|
295
|
+
the reason is shown rather than hidden behind the inspector. */}
|
|
296
|
+
{action.status === "failed" && action.error ? (
|
|
297
|
+
<p data-slot="action-ledger-error" className="text-xs text-destructive">
|
|
298
|
+
{action.error}
|
|
299
|
+
</p>
|
|
300
|
+
) : null}
|
|
301
|
+
|
|
302
|
+
{children}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</li>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The list wrapper. A real list, so its length is announced. */
|
|
310
|
+
export function ActionLedgerList({ className, ...props }: ComponentPropsWithRef<"ul">) {
|
|
311
|
+
return (
|
|
312
|
+
<ul
|
|
313
|
+
data-slot="action-ledger-list"
|
|
314
|
+
className={cn("flex list-none flex-col gap-2", className)}
|
|
315
|
+
{...props}
|
|
316
|
+
/>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Collapsed payload for one entry: the arguments, the result, the diff. */
|
|
321
|
+
export function ActionLedgerPayload({
|
|
322
|
+
className,
|
|
323
|
+
label = "Details",
|
|
324
|
+
children,
|
|
325
|
+
...props
|
|
326
|
+
}: ComponentPropsWithRef<typeof CollapsiblePrimitive.Root> & { label?: string }) {
|
|
327
|
+
return (
|
|
328
|
+
<CollapsiblePrimitive.Root
|
|
329
|
+
data-slot="action-ledger-payload"
|
|
330
|
+
className={cn("mt-1", className)}
|
|
331
|
+
{...props}
|
|
332
|
+
>
|
|
333
|
+
<CollapsiblePrimitive.Trigger
|
|
334
|
+
className={cn(
|
|
335
|
+
"rounded-md text-xs text-muted-foreground transition-colors hover:text-foreground",
|
|
336
|
+
focusRing,
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
339
|
+
{label}
|
|
340
|
+
</CollapsiblePrimitive.Trigger>
|
|
341
|
+
<CollapsiblePrimitive.Content className="pt-2">{children}</CollapsiblePrimitive.Content>
|
|
342
|
+
</CollapsiblePrimitive.Root>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A summary of what is selected and what it will and will not undo.
|
|
348
|
+
*
|
|
349
|
+
* This exists because "Undo 4 selected" is not enough information when one of
|
|
350
|
+
* the four can only be compensated. Saying so before the click is the whole
|
|
351
|
+
* difference between an honest control and a misleading one.
|
|
352
|
+
*/
|
|
353
|
+
export function ActionLedgerSelectionSummary({
|
|
354
|
+
className,
|
|
355
|
+
...props
|
|
356
|
+
}: ComponentPropsWithRef<"p">) {
|
|
357
|
+
const { actions, selected } = useLedgerContext("ActionLedgerSelectionSummary");
|
|
358
|
+
|
|
359
|
+
const chosen = actions.filter((action) => selected.has(action.id));
|
|
360
|
+
const compensable = chosen.filter((a) => a.reversibility === "compensable").length;
|
|
361
|
+
|
|
362
|
+
if (chosen.length === 0) return null;
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<p
|
|
366
|
+
data-slot="action-ledger-selection-summary"
|
|
367
|
+
// Selection changes are user-driven and the wording matters, so it is
|
|
368
|
+
// announced politely rather than silently updating under the button.
|
|
369
|
+
aria-live="polite"
|
|
370
|
+
className={cn("text-xs text-muted-foreground", className)}
|
|
371
|
+
{...props}
|
|
372
|
+
>
|
|
373
|
+
{`${String(chosen.length)} selected`}
|
|
374
|
+
{compensable > 0
|
|
375
|
+
? `, of which ${String(compensable)} can only be offset by a further action, not undone`
|
|
376
|
+
: ""}
|
|
377
|
+
</p>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { ledgerEntryVariants };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ActionLedger,
|
|
3
|
+
ActionLedgerEntry,
|
|
4
|
+
ActionLedgerList,
|
|
5
|
+
ActionLedgerPayload,
|
|
6
|
+
ActionLedgerSelectionSummary,
|
|
7
|
+
ActionLedgerToolbar,
|
|
8
|
+
ledgerEntryVariants,
|
|
9
|
+
type ActionLedgerEntryProps,
|
|
10
|
+
type ActionLedgerProps,
|
|
11
|
+
type ActionStatus,
|
|
12
|
+
type LedgerAction,
|
|
13
|
+
type Reversibility,
|
|
14
|
+
} from "./ai-action-ledger";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineMeta } from "@/registry/schema";
|
|
2
|
+
|
|
3
|
+
export const meta = defineMeta({
|
|
4
|
+
name: "ai-action-ledger",
|
|
5
|
+
title: "AI Action Ledger",
|
|
6
|
+
description: "What an agent actually did, classified by what can be undone.",
|
|
7
|
+
category: "ai",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: ["class-variance-authority", "radix-ui"],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["ai-action-ledger.tsx"],
|
|
12
|
+
a11y:
|
|
13
|
+
"Reversibility is stated in words on every entry, never by colour or icon alone — it decides " +
|
|
14
|
+
"whether the undo control means anything, so it cannot be decoration. Selection uses native " +
|
|
15
|
+
"checkboxes with real labels, and the revert button carries the count in its accessible name " +
|
|
16
|
+
"so it says what pressing it will do. The selection summary is an aria-live region because " +
|
|
17
|
+
"the wording changes as the choice changes, and warns before the click when something can " +
|
|
18
|
+
"only be offset rather than undone.",
|
|
19
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineMeta } from "@/registry/schema";
|
|
2
|
+
|
|
3
|
+
export const meta = defineMeta({
|
|
4
|
+
name: "ai-agent-plan",
|
|
5
|
+
title: "AI Agent Plan",
|
|
6
|
+
description: "What an agent intends to do — including when it changes its mind mid-run.",
|
|
7
|
+
category: "ai",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["ai-agent-plan.tsx"],
|
|
12
|
+
a11y:
|
|
13
|
+
"An ordered list, because a plan is a sequence and the order carries meaning an unordered " +
|
|
14
|
+
'list would discard. The running step carries aria-current="step", which marks the reader\'s ' +
|
|
15
|
+
"place without moving focus and fighting anyone reading ahead. Every status is stated in text " +
|
|
16
|
+
"as well as drawn, and the markers are aria-hidden so the status is not heard twice per step. " +
|
|
17
|
+
"The live region reports structural revisions only — announcing every status transition would " +
|
|
18
|
+
"talk over the reader continuously on a plan of any length.",
|
|
19
|
+
});
|