@calliopelabs/ui 0.11.0 → 0.12.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/README.md +13 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/provider/messages.d.ts +18 -0
- package/dist/provider/messages.js +33 -0
- package/dist/workflow/WorkflowQuestionCard.d.ts +18 -0
- package/dist/workflow/WorkflowQuestionCard.js +78 -0
- package/dist/workflow/question.d.ts +26 -0
- package/dist/workflow/question.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -284,6 +284,19 @@ The package fetches nothing. `DataTable` reads a `TableController`: rows, state,
|
|
|
284
284
|
|
|
285
285
|
## Workflows
|
|
286
286
|
|
|
287
|
+
`WorkflowQuestionCard` shows a host-owned runtime question, including several
|
|
288
|
+
fields, optional choices and Unicode answers. Bind `question.id`, its opaque
|
|
289
|
+
`revision`, `answers` and delivery `state` to the same request. `onSubmit` reports
|
|
290
|
+
answer intent only; the host must authorize delivery and set `submitting` before
|
|
291
|
+
starting its request. Use `accepted` when the service accepted the answer and
|
|
292
|
+
`acknowledged` only with correlated worker evidence. For uncertain delivery,
|
|
293
|
+
`onRefresh` reads the original request without submitting it again. A changed
|
|
294
|
+
question identity or revision requires a fresh host-owned answer draft.
|
|
295
|
+
|
|
296
|
+
The card handles loading, empty, read-only, expired and unavailable states. Its
|
|
297
|
+
copy is overridable through the `workflowQuestion` message group. It never starts
|
|
298
|
+
work, grants permission, fetches data or infers acknowledgment from a click.
|
|
299
|
+
|
|
287
300
|
The workflow surfaces are module-neutral: software, content, media, legal, document review, process, paperwork and automations are labels and versions the host supplies, and nothing in the model assumes tickets, repositories or code. They are host-driven: data in, callbacks out. The package never fetches, saves, authorizes or runs anything.
|
|
288
301
|
|
|
289
302
|
- **Gallery.** `WorkflowGallery` is a full page of `WorkflowCard`s with search, a module filter, scope and status filters, grid and list views, and create and import when the host passes their handlers. Its filters are controlled (`filters`, `onFiltersChange`): a host that searches on its server passes them to its query; one that holds every definition uses `filterWorkflows(workflows, filters)`. Empty and no-matches are different states.
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/Tabs.js";
|
|
|
64
64
|
export { Skeleton } from "./primitives/Skeleton.js";
|
|
65
65
|
export type { WorkflowAction, WorkflowAvailability, WorkflowModule, WorkflowPermissions, WorkflowScope, WorkflowStatus, WorkflowSummary, } from "./workflow/model.js";
|
|
66
66
|
export { WorkflowCard, WorkflowCardSkeleton, type WorkflowCardActions, type WorkflowCardProps, } from "./workflow/WorkflowCard.js";
|
|
67
|
+
export { WorkflowQuestionCard, type WorkflowQuestionCardProps, } from "./workflow/WorkflowQuestionCard.js";
|
|
68
|
+
export type { WorkflowQuestion, WorkflowQuestionField, WorkflowQuestionState, WorkflowQuestionAnswers, WorkflowQuestionSubmission, } from "./workflow/question.js";
|
|
67
69
|
export { ANY_FILTER, DEFAULT_WORKFLOW_FILTERS, filterWorkflows, isFiltered, type WorkflowGalleryFilters, } from "./workflow/gallery.js";
|
|
68
70
|
export { WorkflowGallery, type WorkflowGalleryProps, type WorkflowGalleryView, } from "./workflow/WorkflowGallery.js";
|
|
69
71
|
export { ARTIFACT_TONE, GATE_TONE, RUN_STAGE_TONE, VERDICT_TONE, type Artifact, type ArtifactStatus, type Gate, type GateStatus, type ReviewComment, type ReviewDecision, type ReviewVerdict, type RunStage, type RunStageStatus, type RunValue, } from "./workflow/run.js";
|
package/dist/index.js
CHANGED
|
@@ -74,6 +74,7 @@ export { Popover, PopoverAnchor, PopoverContent, PopoverHeader, PopoverTrigger,
|
|
|
74
74
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/Tabs.js";
|
|
75
75
|
export { Skeleton } from "./primitives/Skeleton.js";
|
|
76
76
|
export { WorkflowCard, WorkflowCardSkeleton, } from "./workflow/WorkflowCard.js";
|
|
77
|
+
export { WorkflowQuestionCard, } from "./workflow/WorkflowQuestionCard.js";
|
|
77
78
|
export { ANY_FILTER, DEFAULT_WORKFLOW_FILTERS, filterWorkflows, isFiltered, } from "./workflow/gallery.js";
|
|
78
79
|
export { WorkflowGallery, } from "./workflow/WorkflowGallery.js";
|
|
79
80
|
export { ARTIFACT_TONE, GATE_TONE, RUN_STAGE_TONE, VERDICT_TONE, } from "./workflow/run.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProductTheme } from "../appearance/product.js";
|
|
2
|
+
import type { WorkflowQuestionState } from "../workflow/question.js";
|
|
2
3
|
import type { WorkflowAction, WorkflowAvailability, WorkflowScope, WorkflowStatus } from "../workflow/model.js";
|
|
3
4
|
import type { ArtifactStatus, GateStatus, ReviewDecision, ReviewVerdict, RunStageStatus } from "../workflow/run.js";
|
|
4
5
|
import type { WorkflowConnectReason, WorkflowNode, WorkflowNodeKind, WorkflowSelector } from "../workflow/definition.js";
|
|
@@ -21,6 +22,23 @@ import type { WorkflowReadError } from "../workflow/portable.js";
|
|
|
21
22
|
* it (plurals, numbers, word order).
|
|
22
23
|
*/
|
|
23
24
|
export interface Messages {
|
|
25
|
+
workflowQuestion: {
|
|
26
|
+
title: string;
|
|
27
|
+
worker: (worker: string) => string;
|
|
28
|
+
state: (state: WorkflowQuestionState) => string;
|
|
29
|
+
description: (state: WorkflowQuestionState) => string;
|
|
30
|
+
loading: string;
|
|
31
|
+
empty: string;
|
|
32
|
+
answer: string;
|
|
33
|
+
answers: string;
|
|
34
|
+
otherAnswers: string;
|
|
35
|
+
multipleHint: string;
|
|
36
|
+
invalidAnswer: string;
|
|
37
|
+
readOnly: string;
|
|
38
|
+
send: string;
|
|
39
|
+
sending: string;
|
|
40
|
+
refresh: string;
|
|
41
|
+
};
|
|
24
42
|
shell: {
|
|
25
43
|
skipToContent: string;
|
|
26
44
|
/** The mobile button and drawer title for the sidebar. */
|
|
@@ -144,6 +144,39 @@ function problemText({ code, values: v }) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
export const defaultMessages = {
|
|
147
|
+
workflowQuestion: {
|
|
148
|
+
title: "Question for this run",
|
|
149
|
+
worker: (worker) => `Worker: ${worker}`,
|
|
150
|
+
state: (state) => ({
|
|
151
|
+
waiting: "Waiting for an answer",
|
|
152
|
+
submitting: "Sending answer",
|
|
153
|
+
accepted: "Answer sent",
|
|
154
|
+
acknowledged: "Worker acknowledged",
|
|
155
|
+
expired: "Answer window closed",
|
|
156
|
+
"delivery-unknown": "Delivery unconfirmed",
|
|
157
|
+
unavailable: "Question unavailable",
|
|
158
|
+
})[state],
|
|
159
|
+
description: (state) => ({
|
|
160
|
+
waiting: "Answer the questions below to continue this task.",
|
|
161
|
+
submitting: "Sending your answer to this task.",
|
|
162
|
+
accepted: "Waiting for the worker to acknowledge your answer.",
|
|
163
|
+
acknowledged: "The worker acknowledged this answer.",
|
|
164
|
+
expired: "This question can no longer accept an answer.",
|
|
165
|
+
"delivery-unknown": "Check delivery before sending another answer.",
|
|
166
|
+
unavailable: "The current question could not be read. Check again to continue.",
|
|
167
|
+
})[state],
|
|
168
|
+
loading: "Loading question…",
|
|
169
|
+
empty: "Nothing needs an answer right now.",
|
|
170
|
+
answer: "Answer",
|
|
171
|
+
answers: "Answers",
|
|
172
|
+
otherAnswers: "Additional answers",
|
|
173
|
+
multipleHint: "Choose options or enter additional answers on separate lines. Up to eight answers.",
|
|
174
|
+
invalidAnswer: "Answer each question with up to 4,000 characters per answer. Multiple answers must be distinct, with no more than eight.",
|
|
175
|
+
readOnly: "An operator can provide these answers.",
|
|
176
|
+
send: "Send answer",
|
|
177
|
+
sending: "Sending answer…",
|
|
178
|
+
refresh: "Check delivery",
|
|
179
|
+
},
|
|
147
180
|
shell: {
|
|
148
181
|
skipToContent: "Skip to content",
|
|
149
182
|
navigation: "Navigation",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { WorkflowQuestion, WorkflowQuestionAnswers, WorkflowQuestionState, WorkflowQuestionSubmission } from "./question.js";
|
|
3
|
+
export interface WorkflowQuestionCardProps {
|
|
4
|
+
question: WorkflowQuestion | null;
|
|
5
|
+
state: WorkflowQuestionState;
|
|
6
|
+
answers?: WorkflowQuestionAnswers;
|
|
7
|
+
onAnswersChange?: (answers: WorkflowQuestionAnswers) => void;
|
|
8
|
+
/** Synchronous intent notification. Set the controlled state before starting delivery. */
|
|
9
|
+
onSubmit?: (submission: WorkflowQuestionSubmission) => void;
|
|
10
|
+
/** Read current delivery state. This action never submits an answer. */
|
|
11
|
+
onRefresh?: () => void;
|
|
12
|
+
canAnswer?: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Controlled projection: a submit callback is never evidence of runtime acknowledgment. */
|
|
18
|
+
export declare function WorkflowQuestionCard(props: WorkflowQuestionCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Button } from "../primitives/Button.js";
|
|
6
|
+
import { Checkbox } from "../primitives/Checkbox.js";
|
|
7
|
+
import { Field } from "../primitives/Field.js";
|
|
8
|
+
import { Textarea } from "../primitives/Textarea.js";
|
|
9
|
+
import { Skeleton } from "../primitives/Skeleton.js";
|
|
10
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
11
|
+
import { StatusBadge } from "../status/StatusBadge.js";
|
|
12
|
+
const TONE = {
|
|
13
|
+
waiting: "warn",
|
|
14
|
+
submitting: "info",
|
|
15
|
+
accepted: "info",
|
|
16
|
+
acknowledged: "ok",
|
|
17
|
+
expired: "muted",
|
|
18
|
+
"delivery-unknown": "warn",
|
|
19
|
+
unavailable: "error",
|
|
20
|
+
};
|
|
21
|
+
function textValid(value) {
|
|
22
|
+
return value.trim().length > 0 && Array.from(value).length <= 4000 && !value.includes("\0");
|
|
23
|
+
}
|
|
24
|
+
function answerValid(field, value) {
|
|
25
|
+
if (field.multiple)
|
|
26
|
+
return (value !== undefined &&
|
|
27
|
+
typeof value !== "string" &&
|
|
28
|
+
value.length > 0 &&
|
|
29
|
+
value.length <= 8 &&
|
|
30
|
+
new Set(value).size === value.length &&
|
|
31
|
+
value.every(textValid));
|
|
32
|
+
return typeof value === "string" && textValid(value);
|
|
33
|
+
}
|
|
34
|
+
/** Controlled projection: a submit callback is never evidence of runtime acknowledgment. */
|
|
35
|
+
export function WorkflowQuestionCard(props) {
|
|
36
|
+
return (_jsx(QuestionBody, { ...props }, JSON.stringify([props.question?.id, props.question?.revision])));
|
|
37
|
+
}
|
|
38
|
+
function QuestionBody({ question, state, answers = {}, onAnswersChange, onSubmit, onRefresh, canAnswer = true, reason, loading = false, className, }) {
|
|
39
|
+
const t = useUI().messages.workflowQuestion;
|
|
40
|
+
const id = React.useId();
|
|
41
|
+
const [attempted, setAttempted] = React.useState(false);
|
|
42
|
+
const editable = !loading && state === "waiting" && canAnswer && !!onSubmit && !!onAnswersChange;
|
|
43
|
+
const set = (field, value) => {
|
|
44
|
+
if (editable)
|
|
45
|
+
onAnswersChange?.({ ...answers, [field]: value });
|
|
46
|
+
};
|
|
47
|
+
const submit = (event) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
if (!editable || !question?.fields.length)
|
|
50
|
+
return;
|
|
51
|
+
setAttempted(true);
|
|
52
|
+
if (question.fields.some((field) => !answerValid(field, answers[field.id])))
|
|
53
|
+
return;
|
|
54
|
+
const selected = Object.fromEntries(question.fields.map((field) => {
|
|
55
|
+
const value = answers[field.id];
|
|
56
|
+
return [field.id, typeof value === "string" ? value : [...value]];
|
|
57
|
+
}));
|
|
58
|
+
onSubmit?.({ questionId: question.id, revision: question.revision, answers: selected });
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("section", { "data-slot": "workflow-question-card", "aria-labelledby": `${id}-title`, "aria-busy": loading || state === "submitting", className: cn("space-y-4 rounded-lg border bg-card p-4 text-card-foreground", className), children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsx("h3", { id: `${id}-title`, className: "font-medium break-words", children: question?.title ?? t.title }), question?.worker && (_jsx("p", { className: "text-sm text-muted-foreground", children: t.worker(question.worker) }))] }), !loading && (_jsx(StatusBadge, { tone: TONE[state], pulse: state === "submitting" || state === "accepted", children: t.state(state) }))] }), _jsx("p", { role: "status", className: "text-sm text-muted-foreground", children: loading ? t.loading : t.description(state) }), reason && _jsx("p", { className: "text-sm text-muted-foreground", children: reason }), loading ? (_jsxs("div", { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-5 w-1/2" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : !question?.fields.length ? (state === "waiting" ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t.empty })) : null) : (_jsxs("form", { onSubmit: submit, noValidate: true, className: "space-y-5", children: [question.fields.map((field, index) => {
|
|
61
|
+
const value = answers[field.id];
|
|
62
|
+
const selected = value !== undefined && typeof value !== "string" ? value : [];
|
|
63
|
+
const labels = new Set(field.options?.map((option) => option.label));
|
|
64
|
+
const custom = selected.filter((answer) => !labels.has(answer));
|
|
65
|
+
const error = attempted && !answerValid(field, value) ? t.invalidAnswer : undefined;
|
|
66
|
+
return (_jsxs("fieldset", { disabled: !editable, className: "min-w-0 space-y-3", children: [_jsx("legend", { className: "mb-3 text-sm font-medium break-words whitespace-pre-wrap", children: field.text }), !!field.options?.length && (_jsx("div", { className: "flex flex-wrap gap-2", children: field.options.map((option, optionIndex) => field.multiple ? (_jsxs("label", { htmlFor: `${id}-${index}-${optionIndex}`, className: "flex max-w-full min-w-0 items-start gap-2 rounded-lg border p-2 text-sm", children: [_jsx(Checkbox, { id: `${id}-${index}-${optionIndex}`, disabled: !editable, checked: selected.includes(option.label), onCheckedChange: (checked) => set(field.id, checked === true
|
|
67
|
+
? [
|
|
68
|
+
...selected.filter((answer) => answer !== option.label),
|
|
69
|
+
option.label,
|
|
70
|
+
]
|
|
71
|
+
: selected.filter((answer) => answer !== option.label)) }), _jsxs("span", { className: "min-w-0 break-words", children: [option.label, option.description && (_jsxs("span", { className: "mt-1 block text-xs text-muted-foreground", children: [" ", option.description] }))] })] }, option.label)) : (_jsx(Button, { type: "button", variant: "outline", disabled: !editable, className: "h-auto max-w-full min-w-0 py-2 text-left", "aria-pressed": value === option.label, onClick: () => set(field.id, option.label), children: _jsxs("span", { className: "min-w-0 break-words whitespace-normal", children: [option.label, option.description && (_jsxs("span", { className: "mt-1 block text-xs text-muted-foreground", children: [" ", option.description] }))] }) }, option.label))) })), _jsx(Field, { label: field.multiple ? (field.options?.length ? t.otherAnswers : t.answers) : t.answer, hint: field.multiple ? t.multipleHint : undefined, error: error, children: _jsx(Textarea, { readOnly: !editable, disabled: !editable, rows: 3, value: field.multiple ? custom.join("\n") : typeof value === "string" ? value : "", onChange: (event) => set(field.id, field.multiple
|
|
72
|
+
? [
|
|
73
|
+
...selected.filter((answer) => labels.has(answer)),
|
|
74
|
+
...(event.target.value === "" ? [] : event.target.value.split("\n")),
|
|
75
|
+
]
|
|
76
|
+
: event.target.value) }) })] }, field.id));
|
|
77
|
+
}), !canAnswer && state === "waiting" && !reason && (_jsx("p", { className: "text-sm text-muted-foreground", children: t.readOnly })), onSubmit && (state === "waiting" || state === "submitting") && (_jsx(Button, { type: "submit", disabled: !editable, children: state === "submitting" ? t.sending : t.send }))] })), onRefresh && (state === "delivery-unknown" || state === "unavailable") && (_jsx(Button, { type: "button", variant: "outline", onClick: onRefresh, children: t.refresh }))] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Runtime question state supplied by the host, independently of form submission. */
|
|
2
|
+
export type WorkflowQuestionState = "waiting" | "submitting" | "accepted" | "acknowledged" | "expired" | "delivery-unknown" | "unavailable";
|
|
3
|
+
export interface WorkflowQuestionField {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
options?: readonly {
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}[];
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WorkflowQuestion {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Opaque host revision, echoed unchanged with the answer intent. */
|
|
15
|
+
revision: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
worker?: string;
|
|
18
|
+
fields: readonly WorkflowQuestionField[];
|
|
19
|
+
}
|
|
20
|
+
export type WorkflowQuestionAnswers = Readonly<Record<string, string | readonly string[]>>;
|
|
21
|
+
/** Intent only. The host owns authorization, delivery correlation and acknowledgment. */
|
|
22
|
+
export interface WorkflowQuestionSubmission {
|
|
23
|
+
questionId: string;
|
|
24
|
+
revision: string;
|
|
25
|
+
answers: WorkflowQuestionAnswers;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calliopelabs/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Calliope AI shared operator UI: app shell, navigation, page frame, data table and list patterns, status colours, design tokens and appearance for Astrolift AI and Zentinelle AI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calliope-ai",
|