@atscript/vue-wf 0.1.63 → 0.1.64

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 CHANGED
@@ -17,6 +17,7 @@ Part of the [atscript-ui](https://github.com/moostjs/atscript-ui) monorepo.
17
17
  - `<AsWfForm>` — single component that drives the full workflow: posts current state, renders the next step's form, validates with the schema returned by the server, and resumes after pauses
18
18
  - Hooks for custom transport, error display, and per-step UI overrides
19
19
  - Built on [`@atscript/vue-form`](../vue-form), so all form-rendering primitives (field types, default renderers, layout grid) carry over
20
+ - Default `AsWfFinish` screen + `wf.finish.*` scoped slots + `navigate` prop + `@dismiss` / `@action` events for the unified `WfFinished` terminal envelope. See [Finish Screens](https://ui.atscript.dev/workflows/finish-screens).
20
21
 
21
22
  ## Install
22
23
 
@@ -0,0 +1,206 @@
1
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeStyle, onMounted, onUnmounted, openBlock, ref, renderList, renderSlot, toDisplayString, watch } from "vue";
2
+ //#region src/components/defaults/as-wf-finish.vue?vue&type=script&setup=true&lang.ts
3
+ const _hoisted_1 = {
4
+ key: 0,
5
+ class: "sr-only",
6
+ "aria-live": "polite"
7
+ };
8
+ const _hoisted_2 = ["data-level"];
9
+ const _hoisted_3 = {
10
+ key: 0,
11
+ class: "as-wf-finish-actions"
12
+ };
13
+ const _hoisted_4 = { class: "as-wf-finish-skip-label" };
14
+ const _hoisted_5 = {
15
+ class: "as-wf-finish-countdown",
16
+ "aria-live": "polite"
17
+ };
18
+ const _hoisted_6 = {
19
+ key: 2,
20
+ class: "as-wf-finish-actions"
21
+ };
22
+ const _hoisted_7 = ["autofocus", "onClick"];
23
+ //#endregion
24
+ //#region src/components/defaults/as-wf-finish.vue
25
+ var as_wf_finish_default = /* @__PURE__ */ defineComponent({
26
+ __name: "as-wf-finish",
27
+ props: {
28
+ payload: {
29
+ type: [Object, null],
30
+ required: true
31
+ },
32
+ navigate: {
33
+ type: Function,
34
+ required: false
35
+ }
36
+ },
37
+ emits: ["dismiss", "action"],
38
+ setup(__props, { emit: __emit }) {
39
+ const props = __props;
40
+ const emit = __emit;
41
+ async function dispatchRedirect(url) {
42
+ if (props.navigate) {
43
+ await props.navigate(url);
44
+ return;
45
+ }
46
+ const loc = globalThis.location;
47
+ if (loc?.assign) {
48
+ loc.assign(url);
49
+ return;
50
+ }
51
+ console.error(`[AsWfFinish] Cannot redirect to "${url}": no \`navigate\` prop and no browser environment. Pass a \`navigate\` prop to AsWfForm / AsWfFinish.`);
52
+ }
53
+ function runAction(action) {
54
+ emit("action", action);
55
+ if (action.type === "redirect") {
56
+ dispatchRedirect(action.target);
57
+ return;
58
+ }
59
+ if (action.type === "reload") {
60
+ window.location.reload();
61
+ return;
62
+ }
63
+ if (action.type === "dismiss") emit("dismiss");
64
+ }
65
+ const end = computed(() => props.payload?.end ?? null);
66
+ const message = computed(() => props.payload?.message ?? null);
67
+ let autoTimer;
68
+ let countdownInterval;
69
+ let autoStartedAt = 0;
70
+ const totalSeconds = ref(0);
71
+ const secondsRemaining = ref(0);
72
+ const autoCancelled = ref(false);
73
+ function clearAutoTimers() {
74
+ if (autoTimer !== void 0) {
75
+ clearTimeout(autoTimer);
76
+ autoTimer = void 0;
77
+ }
78
+ if (countdownInterval !== void 0) {
79
+ clearInterval(countdownInterval);
80
+ countdownInterval = void 0;
81
+ }
82
+ }
83
+ function startAutoTimer() {
84
+ const e = end.value;
85
+ if (!e || e.mode !== "auto") return;
86
+ clearAutoTimers();
87
+ autoCancelled.value = false;
88
+ autoStartedAt = Date.now();
89
+ totalSeconds.value = Math.ceil(e.timeoutMs / 1e3);
90
+ secondsRemaining.value = totalSeconds.value;
91
+ autoTimer = setTimeout(() => {
92
+ clearAutoTimers();
93
+ runAction(e.action);
94
+ }, e.timeoutMs);
95
+ countdownInterval = setInterval(() => {
96
+ const elapsed = Date.now() - autoStartedAt;
97
+ const remainMs = Math.max(0, e.timeoutMs - elapsed);
98
+ const next = Math.ceil(remainMs / 1e3);
99
+ if (next !== secondsRemaining.value) secondsRemaining.value = next;
100
+ }, 250);
101
+ }
102
+ function skipAuto() {
103
+ const e = end.value;
104
+ if (!e || e.mode !== "auto") return;
105
+ const behavior = e.skipButton?.behavior ?? "now";
106
+ clearAutoTimers();
107
+ if (behavior === "now") runAction(e.action);
108
+ else autoCancelled.value = true;
109
+ }
110
+ function cancelAuto() {
111
+ clearAutoTimers();
112
+ autoCancelled.value = true;
113
+ }
114
+ function applyMode() {
115
+ const e = end.value;
116
+ if (!e) return;
117
+ if (e.mode === "immediate") {
118
+ runAction(e.action);
119
+ return;
120
+ }
121
+ if (e.mode === "auto") startAutoTimer();
122
+ }
123
+ onMounted(() => applyMode());
124
+ watch(() => props.payload, (next, prev) => {
125
+ if (next === prev) return;
126
+ clearAutoTimers();
127
+ applyMode();
128
+ });
129
+ onUnmounted(() => clearAutoTimers());
130
+ const manualPrimary = computed(() => end.value?.mode === "manual" ? end.value.primary ?? null : null);
131
+ const manualOptions = computed(() => end.value?.mode === "manual" ? end.value.options ?? [] : []);
132
+ function onKeydown(ev) {
133
+ if (ev.key !== "Enter") return;
134
+ const target = manualPrimary.value ?? manualOptions.value[0] ?? null;
135
+ if (!target) return;
136
+ ev.preventDefault();
137
+ runAction(target.action);
138
+ }
139
+ function triggerButton(btn) {
140
+ runAction(btn.action);
141
+ }
142
+ const skipScope = computed(() => {
143
+ const e = end.value;
144
+ if (e?.mode !== "auto" || !e.skipButton) return null;
145
+ const behavior = e.skipButton.behavior ?? "now";
146
+ return {
147
+ label: e.skipButton.label,
148
+ behavior
149
+ };
150
+ });
151
+ return (_ctx, _cache) => {
152
+ return end.value?.mode === "immediate" ? (openBlock(), createElementBlock("span", _hoisted_1, "Redirecting…")) : (openBlock(), createElementBlock("div", {
153
+ key: 1,
154
+ class: "as-wf-finish",
155
+ onKeydown
156
+ }, [
157
+ message.value ? renderSlot(_ctx.$slots, "message", {
158
+ key: 0,
159
+ message: message.value
160
+ }, () => [createElementVNode("div", {
161
+ class: "as-wf-finish-message",
162
+ "data-level": message.value.level,
163
+ role: "status"
164
+ }, toDisplayString(message.value.text), 9, _hoisted_2)]) : createCommentVNode("v-if", true),
165
+ end.value?.mode === "auto" && !autoCancelled.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [skipScope.value ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "skip", {
166
+ button: skipScope.value,
167
+ trigger: skipAuto
168
+ }, () => [createElementVNode("button", {
169
+ type: "button",
170
+ class: "as-wf-finish-skip",
171
+ style: normalizeStyle({ "--progress-duration": `${end.value.timeoutMs}ms` }),
172
+ onClick: skipAuto
173
+ }, [_cache[1] || (_cache[1] = createElementVNode("span", { class: "as-wf-finish-skip-fill" }, null, -1)), createElementVNode("span", _hoisted_4, toDisplayString(skipScope.value.label), 1)], 4)])])) : createCommentVNode("v-if", true), renderSlot(_ctx.$slots, "countdown", {
174
+ secondsRemaining: secondsRemaining.value,
175
+ totalSeconds: totalSeconds.value,
176
+ skip: skipAuto,
177
+ cancel: cancelAuto
178
+ }, () => [createElementVNode("div", _hoisted_5, " Continuing in " + toDisplayString(secondsRemaining.value) + "… ", 1)])], 64)) : createCommentVNode("v-if", true),
179
+ end.value?.mode === "manual" ? (openBlock(), createElementBlock("div", _hoisted_6, [manualPrimary.value ? renderSlot(_ctx.$slots, "primary", {
180
+ key: 0,
181
+ button: manualPrimary.value,
182
+ trigger: () => triggerButton(manualPrimary.value)
183
+ }, () => [createElementVNode("button", {
184
+ type: "button",
185
+ class: "as-wf-finish-primary",
186
+ autofocus: "",
187
+ onClick: _cache[0] || (_cache[0] = ($event) => triggerButton(manualPrimary.value))
188
+ }, toDisplayString(manualPrimary.value.label), 1)]) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList(manualOptions.value, (btn, index) => {
189
+ return renderSlot(_ctx.$slots, "option", {
190
+ key: index,
191
+ button: btn,
192
+ index,
193
+ trigger: () => triggerButton(btn)
194
+ }, () => [createElementVNode("button", {
195
+ type: "button",
196
+ class: "as-wf-finish-option",
197
+ autofocus: !manualPrimary.value && index === 0,
198
+ onClick: ($event) => triggerButton(btn)
199
+ }, toDisplayString(btn.label), 9, _hoisted_7)]);
200
+ }), 128))])) : createCommentVNode("v-if", true)
201
+ ], 32));
202
+ };
203
+ }
204
+ });
205
+ //#endregion
206
+ export { as_wf_finish_default as t };
@@ -0,0 +1,2 @@
1
+ import { t as _default } from "./as-wf-finish.vue-BrMzuLaH.mjs";
2
+ export { _default as default };
@@ -0,0 +1,2 @@
1
+ import { t as as_wf_finish_default } from "./as-wf-finish-B7mz8kVT.mjs";
2
+ export { as_wf_finish_default as default };
@@ -0,0 +1,64 @@
1
+ import { a as WfMessage, n as WfButton, r as WfFinished, t as WfAction } from "./index-KRfH1NOi.mjs";
2
+ import * as vue from "vue";
3
+
4
+ //#region src/components/defaults/as-wf-finish.vue.d.ts
5
+ type __VLS_Props = {
6
+ payload: WfFinished | null;
7
+ /**
8
+ * Consumer-provided navigation handler. Receives the redirect target URL —
9
+ * the consumer decides cross-origin vs in-app routing. Mirrors the
10
+ * `navigate` option on `@atscript/db-client`'s `Client`: one handler
11
+ * across the stack.
12
+ */
13
+ navigate?: (url: string) => void | Promise<void>;
14
+ };
15
+ declare function skipAuto(): void;
16
+ declare function cancelAuto(): void;
17
+ declare var __VLS_1: {
18
+ message: WfMessage;
19
+ }, __VLS_3: {
20
+ button: {
21
+ readonly label: string;
22
+ readonly behavior: "now" | "cancel";
23
+ };
24
+ trigger: typeof skipAuto;
25
+ }, __VLS_5: {
26
+ secondsRemaining: number;
27
+ totalSeconds: number;
28
+ skip: typeof skipAuto;
29
+ cancel: typeof cancelAuto;
30
+ }, __VLS_7: {
31
+ button: WfButton;
32
+ trigger: () => void;
33
+ }, __VLS_9: {
34
+ button: WfButton;
35
+ index: number;
36
+ trigger: () => void;
37
+ };
38
+ type __VLS_Slots = {} & {
39
+ message?: (props: typeof __VLS_1) => any;
40
+ } & {
41
+ skip?: (props: typeof __VLS_3) => any;
42
+ } & {
43
+ countdown?: (props: typeof __VLS_5) => any;
44
+ } & {
45
+ primary?: (props: typeof __VLS_7) => any;
46
+ } & {
47
+ option?: (props: typeof __VLS_9) => any;
48
+ };
49
+ declare const __VLS_base: vue.DefineComponent<__VLS_Props, {}, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {} & {
50
+ dismiss: () => any;
51
+ action: (action: WfAction) => any;
52
+ }, string, vue.PublicProps, Readonly<__VLS_Props> & Readonly<{
53
+ onDismiss?: (() => any) | undefined;
54
+ onAction?: ((action: WfAction) => any) | undefined;
55
+ }>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, false, {}, any>;
56
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
57
+ declare const _default: typeof __VLS_export;
58
+ type __VLS_WithSlots<T, S> = T & {
59
+ new (): {
60
+ $slots: S;
61
+ };
62
+ };
63
+ //#endregion
64
+ export { _default as t };
@@ -1,4 +1,5 @@
1
- import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, guardReactiveProps, normalizeProps, onMounted, onUnmounted, openBlock, reactive, ref, renderSlot, shallowRef, toDisplayString, toRaw, unref, watch, withCtx } from "vue";
1
+ import { t as as_wf_finish_default } from "./as-wf-finish-B7mz8kVT.mjs";
2
+ import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createVNode, defineComponent, guardReactiveProps, normalizeProps, onMounted, onUnmounted, openBlock, reactive, ref, renderSlot, shallowRef, toDisplayString, toRaw, unref, watch, withCtx } from "vue";
2
3
  import { WF_ACTION_WITH_DATA, createFormData, createFormDef, getFieldMeta } from "@atscript/ui";
3
4
  import { deserializeAnnotatedType } from "@atscript/typescript/utils";
4
5
  import { AsForm } from "@atscript/vue-form";
@@ -12,6 +13,7 @@ function useWfForm(options) {
12
13
  const formContext = shallowRef({});
13
14
  const errors = shallowRef({});
14
15
  const response = shallowRef(null);
16
+ const finishedPayload = shallowRef(null);
15
17
  const error = shallowRef(null);
16
18
  const loading = ref(false);
17
19
  const finished = ref(false);
@@ -48,6 +50,7 @@ function useWfForm(options) {
48
50
  if (data.finished) {
49
51
  finished.value = true;
50
52
  response.value = data;
53
+ finishedPayload.value = data;
51
54
  formDef.value = null;
52
55
  formData.value = null;
53
56
  lastPayloadJson = void 0;
@@ -56,6 +59,7 @@ function useWfForm(options) {
56
59
  if (data.sent === true || typeof data.outlet === "string") {
57
60
  finished.value = true;
58
61
  response.value = data;
62
+ finishedPayload.value = null;
59
63
  formDef.value = null;
60
64
  formData.value = null;
61
65
  lastPayloadJson = void 0;
@@ -97,14 +101,20 @@ function useWfForm(options) {
97
101
  signal,
98
102
  body: JSON.stringify(buildBody(body))
99
103
  });
104
+ const data = await res.json().catch(() => null);
105
+ if (data && typeof data === "object" && !Array.isArray(data)) extractToken(data);
100
106
  if (!res.ok) {
101
107
  error.value = {
102
- message: (await res.json().catch(() => ({ message: res.statusText }))).message ?? res.statusText,
108
+ message: data?.message ?? res.statusText,
103
109
  status: res.status
104
110
  };
105
111
  return;
106
112
  }
107
- processResponse(await res.json());
113
+ if (!data) {
114
+ error.value = { message: "Unexpected response format" };
115
+ return;
116
+ }
117
+ processResponse(data);
108
118
  } catch (err) {
109
119
  if (err instanceof DOMException && err.name === "AbortError") return;
110
120
  error.value = { message: err instanceof Error ? err.message : "Network error" };
@@ -115,6 +125,9 @@ function useWfForm(options) {
115
125
  async function start(input) {
116
126
  const initialToken = readInitialToken();
117
127
  if (initialToken) token = initialToken;
128
+ if (import.meta.env?.DEV && !initialToken && transport !== "query") {
129
+ if (new URLSearchParams(window.location.search).get(tokenName)) console.warn(`[useWfForm] URL contains ?${tokenName}=… but no \`initialToken\` was passed and \`tokenTransport\` is "${transport}". Workflow will start fresh instead of resuming the magic-link flow. Pass \`initial-token="route.query.${tokenName}"\` or set \`tokenTransport: "query"\`.`);
130
+ }
118
131
  const body = { [wfidName]: options.name };
119
132
  if (input) body.input = input;
120
133
  if (initialToken) body[tokenName] = initialToken;
@@ -146,6 +159,7 @@ function useWfForm(options) {
146
159
  loading,
147
160
  finished,
148
161
  response,
162
+ finishedPayload,
149
163
  error,
150
164
  start,
151
165
  submit,
@@ -156,7 +170,10 @@ function useWfForm(options) {
156
170
  }
157
171
  //#endregion
158
172
  //#region src/components/as-wf-form.vue?vue&type=script&setup=true&lang.ts
159
- const _hoisted_1 = { key: 0 };
173
+ const _hoisted_1 = {
174
+ key: 0,
175
+ class: "as-wf-form-loading"
176
+ };
160
177
  const _hoisted_2 = { key: 1 };
161
178
  const _hoisted_3 = { key: 2 };
162
179
  const _hoisted_4 = { key: 3 };
@@ -186,6 +203,10 @@ var as_wf_form_default = /* @__PURE__ */ defineComponent({
186
203
  type: Function,
187
204
  required: false
188
205
  },
206
+ navigate: {
207
+ type: Function,
208
+ required: false
209
+ },
189
210
  path: {
190
211
  type: String,
191
212
  required: true
@@ -236,7 +257,9 @@ var as_wf_form_default = /* @__PURE__ */ defineComponent({
236
257
  "error",
237
258
  "form",
238
259
  "submit",
239
- "loading"
260
+ "loading",
261
+ "dismiss",
262
+ "action"
240
263
  ],
241
264
  setup(__props, { emit: __emit }) {
242
265
  const props = __props;
@@ -289,10 +312,47 @@ var as_wf_form_default = /* @__PURE__ */ defineComponent({
289
312
  submit: onSubmit,
290
313
  retry: unref(wf).retry
291
314
  }
292
- }, () => [unref(wf).loading.value && !unref(wf).formDef.value ? (openBlock(), createElementBlock("div", _hoisted_1, [renderSlot(_ctx.$slots, "wf.loading")])) : unref(wf).error.value && !unref(wf).formDef.value ? (openBlock(), createElementBlock("div", _hoisted_2, [renderSlot(_ctx.$slots, "wf.error", {
315
+ }, () => [unref(wf).loading.value && !unref(wf).formDef.value ? (openBlock(), createElementBlock("div", _hoisted_1, [renderSlot(_ctx.$slots, "wf.loading", {}, () => [_cache[2] || (_cache[2] = createElementVNode("div", { class: "as-form-overlay" }, [createElementVNode("span", {
316
+ class: "as-form-overlay-icon",
317
+ "aria-hidden": "true"
318
+ })], -1))])])) : unref(wf).error.value && !unref(wf).formDef.value ? (openBlock(), createElementBlock("div", _hoisted_2, [renderSlot(_ctx.$slots, "wf.error", {
293
319
  error: unref(wf).error.value,
294
320
  retry: unref(wf).retry
295
- }, () => [createElementVNode("div", null, toDisplayString(unref(wf).error.value?.message ?? "Error"), 1)])])) : unref(wf).finished.value ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "wf.finished", { response: unref(wf).response.value })])) : unref(wf).formDef.value && unref(wf).formData.value ? (openBlock(), createElementBlock("div", _hoisted_4, [unref(wf).error.value ? renderSlot(_ctx.$slots, "form.error", {
321
+ }, () => [createElementVNode("div", null, toDisplayString(unref(wf).error.value?.message ?? "Error"), 1)])])) : unref(wf).finished.value ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "wf.finished", {
322
+ response: unref(wf).response.value,
323
+ payload: unref(wf).finishedPayload.value
324
+ }, () => [createCommentVNode("\n Per `feedback_vue_empty_slot`: a `<template #x>` registered on a child\n short-circuits the child's default fallback even when its content is\n empty. So we only forward slot names that the AsWfForm consumer\n actually provided — otherwise AsWfFinish's defaults render.\n "), createVNode(as_wf_finish_default, {
325
+ payload: unref(wf).finishedPayload.value,
326
+ navigate: __props.navigate,
327
+ onDismiss: _cache[0] || (_cache[0] = () => emit("dismiss")),
328
+ onAction: _cache[1] || (_cache[1] = (a) => emit("action", a))
329
+ }, createSlots({ _: 2 }, [
330
+ _ctx.$slots["wf.finish.message"] ? {
331
+ name: "message",
332
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "wf.finish.message", normalizeProps(guardReactiveProps(scope)))]),
333
+ key: "0"
334
+ } : void 0,
335
+ _ctx.$slots["wf.finish.countdown"] ? {
336
+ name: "countdown",
337
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "wf.finish.countdown", normalizeProps(guardReactiveProps(scope)))]),
338
+ key: "1"
339
+ } : void 0,
340
+ _ctx.$slots["wf.finish.skip"] ? {
341
+ name: "skip",
342
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "wf.finish.skip", normalizeProps(guardReactiveProps(scope)))]),
343
+ key: "2"
344
+ } : void 0,
345
+ _ctx.$slots["wf.finish.primary"] ? {
346
+ name: "primary",
347
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "wf.finish.primary", normalizeProps(guardReactiveProps(scope)))]),
348
+ key: "3"
349
+ } : void 0,
350
+ _ctx.$slots["wf.finish.option"] ? {
351
+ name: "option",
352
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "wf.finish.option", normalizeProps(guardReactiveProps(scope)))]),
353
+ key: "4"
354
+ } : void 0
355
+ ]), 1032, ["payload", "navigate"])])])) : unref(wf).formDef.value && unref(wf).formData.value ? (openBlock(), createElementBlock("div", _hoisted_4, [unref(wf).error.value ? renderSlot(_ctx.$slots, "form.error", {
296
356
  key: 0,
297
357
  error: unref(wf).error.value,
298
358
  retry: unref(wf).retry
@@ -1,2 +1,2 @@
1
- import { t as _default } from "./as-wf-form.vue-BP739hjJ.mjs";
1
+ import { t as _default } from "./as-wf-form.vue-CBujwX55.mjs";
2
2
  export { _default as default };
@@ -1,2 +1,3 @@
1
- import { t as as_wf_form_default } from "./as-wf-form-CBBvLmgY.mjs";
1
+ import { t as as_wf_form_default } from "./as-wf-form-CxIEWOyU.mjs";
2
+ import "./as-wf-finish-B7mz8kVT.mjs";
2
3
  export { as_wf_form_default as default };
@@ -1,3 +1,4 @@
1
+ import { a as WfMessage, n as WfButton, r as WfFinished, t as WfAction } from "./index-KRfH1NOi.mjs";
1
2
  import * as vue from "vue";
2
3
  import { Component, Ref, ShallowRef } from "vue";
3
4
  import { ClientFactory, FormDef } from "@atscript/ui";
@@ -51,6 +52,8 @@ interface UseWfFormReturn {
51
52
  loading: Ref<boolean>;
52
53
  finished: Ref<boolean>;
53
54
  response: ShallowRef<unknown>;
55
+ /** Typed envelope on finish — null while running, on outlet pause, or before first response. */
56
+ finishedPayload: ShallowRef<WfFinished | null>;
54
57
  error: ShallowRef<unknown>;
55
58
  start: (input?: Record<string, unknown>) => Promise<void>;
56
59
  submit: (data: unknown) => Promise<void>;
@@ -70,6 +73,12 @@ interface AsWfFormProps extends UseWfFormOptions {
70
73
  components?: Record<string, Component>;
71
74
  /** Per-form client factory override (FK value-help). Forwarded to AsForm. */
72
75
  clientFactory?: ClientFactory;
76
+ /**
77
+ * Consumer-provided navigation handler forwarded to `<AsWfFinish>`. Pairs
78
+ * with `@atscript/db-client`'s `Client({ navigate })` option so one handler
79
+ * covers both workflow redirects and DB navigate-actions.
80
+ */
81
+ navigate?: (url: string) => void | Promise<void>;
73
82
  }
74
83
  declare function onSubmit(data: unknown): void;
75
84
  declare var __VLS_1: {
@@ -94,31 +103,52 @@ declare var __VLS_1: {
94
103
  retry: () => Promise<void>;
95
104
  }, __VLS_7: {
96
105
  response: unknown;
97
- }, __VLS_9: {
106
+ payload: WfFinished<unknown> | null;
107
+ }, __VLS_19: {
108
+ message: WfMessage;
109
+ }, __VLS_22: {
110
+ secondsRemaining: number;
111
+ totalSeconds: number;
112
+ skip: () => void;
113
+ cancel: () => void;
114
+ }, __VLS_25: {
115
+ button: {
116
+ readonly label: string;
117
+ readonly behavior: "now" | "cancel";
118
+ };
119
+ trigger: () => void;
120
+ }, __VLS_28: {
121
+ button: WfButton;
122
+ trigger: () => void;
123
+ }, __VLS_31: {
124
+ button: WfButton;
125
+ index: number;
126
+ trigger: () => void;
127
+ }, __VLS_33: {
98
128
  error: {};
99
129
  retry: () => Promise<void>;
100
- }, __VLS_22: {
130
+ }, __VLS_46: {
101
131
  loading: boolean;
102
132
  clearErrors: () => void;
103
133
  reset: () => Promise<void>;
104
134
  setErrors: (errors: Record<string, string>) => void;
105
135
  formContext: Record<string, unknown> | undefined;
106
136
  disabled: boolean;
107
- }, __VLS_25: {
137
+ }, __VLS_49: {
108
138
  loading: boolean;
109
139
  clearErrors: () => void;
110
140
  reset: () => Promise<void>;
111
141
  setErrors: (errors: Record<string, string>) => void;
112
142
  formContext: Record<string, unknown> | undefined;
113
143
  disabled: boolean;
114
- }, __VLS_28: {
144
+ }, __VLS_52: {
115
145
  loading: boolean;
116
146
  clearErrors: () => void;
117
147
  reset: () => Promise<void>;
118
148
  setErrors: (errors: Record<string, string>) => void;
119
149
  disabled: boolean;
120
150
  formContext: Record<string, unknown> | undefined;
121
- }, __VLS_31: {
151
+ }, __VLS_55: {
122
152
  loading: boolean;
123
153
  disabled: boolean;
124
154
  text: string;
@@ -126,7 +156,7 @@ declare var __VLS_1: {
126
156
  reset: () => Promise<void>;
127
157
  setErrors: (errors: Record<string, string>) => void;
128
158
  formContext: Record<string, unknown> | undefined;
129
- }, __VLS_34: {
159
+ }, __VLS_58: {
130
160
  loading: boolean;
131
161
  disabled: boolean;
132
162
  clearErrors: () => void;
@@ -143,33 +173,47 @@ type __VLS_Slots = {} & {
143
173
  } & {
144
174
  'wf.finished'?: (props: typeof __VLS_7) => any;
145
175
  } & {
146
- 'form.error'?: (props: typeof __VLS_9) => any;
176
+ 'wf.finish.message'?: (props: typeof __VLS_19) => any;
177
+ } & {
178
+ 'wf.finish.countdown'?: (props: typeof __VLS_22) => any;
179
+ } & {
180
+ 'wf.finish.skip'?: (props: typeof __VLS_25) => any;
181
+ } & {
182
+ 'wf.finish.primary'?: (props: typeof __VLS_28) => any;
183
+ } & {
184
+ 'wf.finish.option'?: (props: typeof __VLS_31) => any;
147
185
  } & {
148
- 'form.header'?: (props: typeof __VLS_22) => any;
186
+ 'form.error'?: (props: typeof __VLS_33) => any;
149
187
  } & {
150
- 'form.before'?: (props: typeof __VLS_25) => any;
188
+ 'form.header'?: (props: typeof __VLS_46) => any;
151
189
  } & {
152
- 'form.after'?: (props: typeof __VLS_28) => any;
190
+ 'form.before'?: (props: typeof __VLS_49) => any;
153
191
  } & {
154
- 'form.submit'?: (props: typeof __VLS_31) => any;
192
+ 'form.after'?: (props: typeof __VLS_52) => any;
155
193
  } & {
156
- 'form.footer'?: (props: typeof __VLS_34) => any;
194
+ 'form.submit'?: (props: typeof __VLS_55) => any;
195
+ } & {
196
+ 'form.footer'?: (props: typeof __VLS_58) => any;
157
197
  };
158
198
  declare const __VLS_base: vue.DefineComponent<AsWfFormProps, {}, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {} & {
159
- finished: (response: unknown) => any;
199
+ dismiss: () => any;
200
+ action: (action: WfAction) => any;
160
201
  error: (error: {
161
202
  message: string;
162
203
  status?: number;
163
204
  }) => any;
205
+ finished: (response: unknown) => any;
164
206
  submit: (data: unknown) => any;
165
207
  loading: (isLoading: boolean) => any;
166
208
  form: (def: FormDef, context?: Record<string, unknown> | undefined) => any;
167
209
  }, string, vue.PublicProps, Readonly<AsWfFormProps> & Readonly<{
168
- onFinished?: ((response: unknown) => any) | undefined;
210
+ onDismiss?: (() => any) | undefined;
211
+ onAction?: ((action: WfAction) => any) | undefined;
169
212
  onError?: ((error: {
170
213
  message: string;
171
214
  status?: number;
172
215
  }) => any) | undefined;
216
+ onFinished?: ((response: unknown) => any) | undefined;
173
217
  onSubmit?: ((data: unknown) => any) | undefined;
174
218
  onLoading?: ((isLoading: boolean) => any) | undefined;
175
219
  onForm?: ((def: FormDef, context?: Record<string, unknown> | undefined) => any) | undefined;
@@ -0,0 +1,44 @@
1
+ //#region ../moost-wf/src/wf-finished.d.ts
2
+ interface WfFinished<TData = unknown> {
3
+ finished: true;
4
+ data?: TData;
5
+ message?: WfMessage;
6
+ end?: WfFinishedEnd;
7
+ aborted?: boolean;
8
+ reason?: string;
9
+ }
10
+ interface WfMessage {
11
+ level: "info" | "success" | "warn" | "error";
12
+ text: string;
13
+ }
14
+ type WfFinishedEnd = {
15
+ mode: "immediate";
16
+ action: WfAction;
17
+ } | {
18
+ mode: "auto";
19
+ timeoutMs: number;
20
+ action: WfAction;
21
+ skipButton?: {
22
+ label: string;
23
+ behavior?: "now" | "cancel";
24
+ };
25
+ } | {
26
+ mode: "manual";
27
+ primary?: WfButton;
28
+ options?: WfButton[];
29
+ };
30
+ interface WfButton {
31
+ label: string;
32
+ action: WfAction;
33
+ }
34
+ type WfAction = {
35
+ type: "redirect";
36
+ target: string;
37
+ reason?: string;
38
+ } | {
39
+ type: "reload";
40
+ } | {
41
+ type: "dismiss";
42
+ };
43
+ //#endregion
44
+ export { WfMessage as a, WfFinishedEnd as i, WfButton as n, WfFinished as r, WfAction as t };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,4 @@
1
- import { i as useWfForm, n as UseWfFormOptions, r as UseWfFormReturn, t as _default } from "./as-wf-form.vue-BP739hjJ.mjs";
2
- export { _default as AsWfForm, type UseWfFormOptions, type UseWfFormReturn, useWfForm };
1
+ import { a as WfMessage, i as WfFinishedEnd, n as WfButton, r as WfFinished, t as WfAction } from "./index-KRfH1NOi.mjs";
2
+ import { i as useWfForm, n as UseWfFormOptions, r as UseWfFormReturn, t as _default$1 } from "./as-wf-form.vue-CBujwX55.mjs";
3
+ import { t as _default } from "./as-wf-finish.vue-BrMzuLaH.mjs";
4
+ export { _default as AsWfFinish, _default$1 as AsWfForm, type UseWfFormOptions, type UseWfFormReturn, type WfAction, type WfButton, type WfFinished, type WfFinishedEnd, type WfMessage, useWfForm };
package/dist/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import { n as useWfForm, t as as_wf_form_default } from "./as-wf-form-CBBvLmgY.mjs";
2
- export { as_wf_form_default as AsWfForm, useWfForm };
1
+ import { n as useWfForm, t as as_wf_form_default } from "./as-wf-form-CxIEWOyU.mjs";
2
+ import { t as as_wf_finish_default } from "./as-wf-finish-B7mz8kVT.mjs";
3
+ export { as_wf_finish_default as AsWfFinish, as_wf_form_default as AsWfForm, useWfForm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/vue-wf",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "Workflow form integration for Vue 3 — HTTP round-trip loop driven by atscript type metadata",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/index.d.mts",
37
37
  "import": "./dist/index.mjs"
38
38
  },
39
+ "./as-wf-finish": {
40
+ "types": "./dist/as-wf-finish.d.mts",
41
+ "import": "./dist/as-wf-finish.mjs"
42
+ },
39
43
  "./as-wf-form": {
40
44
  "types": "./dist/as-wf-form.d.mts",
41
45
  "import": "./dist/as-wf-form.mjs"
@@ -46,7 +50,7 @@
46
50
  "access": "public"
47
51
  },
48
52
  "dependencies": {
49
- "@atscript/ui": "^0.1.63"
53
+ "@atscript/ui": "^0.1.64"
50
54
  },
51
55
  "devDependencies": {
52
56
  "@atscript/core": "^0.1.56",
@@ -58,13 +62,13 @@
58
62
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
59
63
  "vue": "^3",
60
64
  "vue-tsc": "^3.2.6",
61
- "@atscript/moost-wf": "^0.1.63",
62
- "@atscript/vue-form": "^0.1.63"
65
+ "@atscript/moost-wf": "^0.1.64",
66
+ "@atscript/vue-form": "^0.1.64"
63
67
  },
64
68
  "peerDependencies": {
65
69
  "@atscript/typescript": "^0.1.56",
66
70
  "vue": "^3",
67
- "@atscript/vue-form": "^0.1.63"
71
+ "@atscript/vue-form": "^0.1.64"
68
72
  },
69
73
  "scripts": {
70
74
  "build": "node ../../scripts/gen-exports.mjs && vp pack",