@parallelworks/ui 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +201 -0
  2. package/dist/ai/components/ChatEmptyState.js +71 -86
  3. package/dist/ai/components/ChatInput.d.ts +11 -3
  4. package/dist/ai/components/ChatInput.js +19 -11
  5. package/dist/ai/components/ChatSidebar.js +12 -130
  6. package/dist/ai/components/ChatStage.d.ts +11 -0
  7. package/dist/ai/components/ChatStage.js +26 -0
  8. package/dist/ai/components/ComposerChrome.d.ts +3 -0
  9. package/dist/ai/components/ComposerChrome.js +21 -2
  10. package/dist/ai/components/conversationActions.d.ts +23 -0
  11. package/dist/ai/components/conversationActions.js +108 -0
  12. package/dist/ai/index.d.ts +4 -3
  13. package/dist/ai/index.js +6 -5
  14. package/dist/ai/strings.d.ts +1 -0
  15. package/dist/ai/strings.js +2 -1
  16. package/dist/ai/ui/ConversationSidebar.d.ts +3 -2
  17. package/dist/ai/ui/ConversationSidebar.js +2 -1
  18. package/dist/ai/ui/sidebar.d.ts +6 -0
  19. package/dist/ai/ui/sidebar.js +10 -1
  20. package/dist/components/Dropdown.js +3 -1
  21. package/dist/components/FilterPill.js +1 -0
  22. package/dist/components/Provider.d.ts +14 -0
  23. package/dist/components/Provider.js +14 -3
  24. package/dist/components/SegmentedControl.d.ts +24 -0
  25. package/dist/components/SegmentedControl.js +69 -0
  26. package/dist/editing.d.ts +32 -0
  27. package/dist/editor/FormCodePanel.js +1 -1
  28. package/dist/engine.d.ts +3 -0
  29. package/dist/form/FieldWrapper.js +1 -1
  30. package/dist/form/Fields/CheckboxGroupField.d.ts +3 -3
  31. package/dist/form/Fields/CheckboxGroupField.js +10 -4
  32. package/dist/form/Fields/RadioField.d.ts +5 -3
  33. package/dist/form/Fields/RadioField.js +25 -17
  34. package/dist/form/Fields/fieldOption.d.ts +12 -0
  35. package/dist/form/Fields/fieldOption.js +17 -0
  36. package/dist/form/Form.d.ts +4 -2
  37. package/dist/form/Form.js +170 -68
  38. package/dist/form/FormLayout.d.ts +7 -0
  39. package/dist/form/FormLayout.js +144 -0
  40. package/dist/form/SpecialTypes/FormikCustomDropdown.js +1 -1
  41. package/dist/form/SpecialTypes/FormikCustomInput.js +7 -0
  42. package/dist/form/Wizard/WizardContainer.d.ts +1 -1
  43. package/dist/form/Wizard/WizardContainer.js +244 -49
  44. package/dist/form/Wizard/WizardNavigation.js +4 -4
  45. package/dist/form/Wizard/WizardStepContent.d.ts +1 -1
  46. package/dist/form/Wizard/WizardStepContent.js +19 -10
  47. package/dist/form/Wizard/WizardStepIndicator.js +35 -7
  48. package/dist/form/Wizard/types.d.ts +19 -6
  49. package/dist/form/Wizard/useWizardState.d.ts +1 -0
  50. package/dist/form/Wizard/useWizardState.js +5 -1
  51. package/dist/form/Wizard/utils.d.ts +7 -0
  52. package/dist/form/Wizard/utils.js +18 -1
  53. package/dist/form/index.d.ts +2 -0
  54. package/dist/form/index.js +4 -2
  55. package/dist/form/layout.d.ts +50 -0
  56. package/dist/form/layout.js +125 -0
  57. package/dist/form/layoutCSS.d.ts +10 -0
  58. package/dist/form/layoutCSS.js +234 -0
  59. package/dist/form/lib.d.ts +2 -0
  60. package/dist/form/lib.js +17 -2
  61. package/dist/form/types/fieldTypes.d.ts +8 -23
  62. package/dist/graph/DependencyGraph.d.ts +20 -1
  63. package/dist/graph/DependencyGraph.js +437 -142
  64. package/dist/graph/MatrixGroup.d.ts +3 -1
  65. package/dist/graph/MatrixGroup.js +31 -28
  66. package/dist/graph/TreeView.js +6 -6
  67. package/dist/graph/gridSpacing.d.ts +7 -0
  68. package/dist/graph/gridSpacing.js +10 -0
  69. package/dist/graph/util.d.ts +2 -0
  70. package/dist/graph/util.js +2 -2
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.js +2 -1
  73. package/dist/list/ListViewControls.d.ts +15 -0
  74. package/dist/list/ListViewControls.js +9 -3
  75. package/dist/list/index.d.ts +1 -1
  76. package/dist/list/index.js +2 -2
  77. package/dist/styles/base.css +3 -1
  78. package/dist/styles/theme.css +5 -0
  79. package/dist/styles.css +1 -1
  80. package/dist/theme/index.d.ts +2 -0
  81. package/dist/theme/index.js +3 -2
  82. package/package.json +4 -2
  83. package/dist/ai/components/ComposerFrame.d.ts +0 -16
  84. package/dist/ai/components/ComposerFrame.js +0 -33
@@ -1,49 +1,172 @@
1
+ import { useStrings, useWorkflowEngine } from "../../components/Provider.js";
2
+ import { copyBounds, stepText } from "./utils.js";
3
+ import { initializeValues } from "../lib.js";
4
+ import { getValueUsingPath } from "../utils/getValueUsingPath.js";
1
5
  import { useWizardState } from "./useWizardState.js";
2
6
  import { WizardNavigation } from "./WizardNavigation.js";
3
7
  import { WizardStepContent } from "./WizardStepContent.js";
4
8
  import { WizardStepIndicator } from "./WizardStepIndicator.js";
5
- import { useCallback } from "react";
9
+ import { useCallback, useEffect, useMemo, useRef } from "react";
6
10
  import { jsx, jsxs } from "react/jsx-runtime";
7
11
  import { useFormikContext } from "formik";
8
12
  //#region src/form/Wizard/WizardContainer.tsx
9
- function hasNestedError(stepErrors, fieldName) {
10
- return typeof stepErrors === "object" && stepErrors !== null && fieldName in stepErrors;
11
- }
12
- function WizardContainer({ wizardConfig, values, onChange, onSubmit, className = "", labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false, setFieldValue, setFieldTouched }) {
13
- const { validateForm, setTouched, touched } = useFormikContext();
13
+ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className = "", labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false, nested = false, fieldNamePrefix = "", setFieldValue, setFieldTouched }) {
14
+ const { validateForm, setTouched, touched, initialValues } = useFormikContext();
15
+ const { form: strings } = useStrings();
16
+ const engine = useWorkflowEngine();
14
17
  const { config, steps, stepOrder } = wizardConfig;
15
- const validateCurrentStep = useCallback(async (stepKey) => {
16
- const stepConfig = steps[stepKey];
17
- if (!stepConfig?.options) return true;
18
- const stepFieldNames = Object.keys(stepConfig.options);
19
- const shouldFlatten = config.flatten !== false;
18
+ const reads = useRef(/* @__PURE__ */ new Map());
19
+ const perCopy = useCallback((value, copy, numbered) => {
20
+ if (Array.isArray(value)) return value[copy] ?? (numbered ? strings.copyTitle(value[0] ?? "", copy + 1) : "");
21
+ const text = value ?? "";
22
+ if (text.includes("${{")) {
23
+ const key = JSON.stringify([text, copy]);
24
+ const inputs = JSON.stringify([...engine.inputDependencies(text).inputDeps].map((dep) => [dep, getValueUsingPath(values, dep)]));
25
+ const last = reads.current.get(key);
26
+ if (last?.inputs === inputs) return last.read;
27
+ const result = engine.evaluate({
28
+ inputs: values,
29
+ obj: { text },
30
+ orgVars: {},
31
+ index: copy
32
+ });
33
+ const read = String(result.text ?? "");
34
+ if (reads.current.size >= 500) reads.current.clear();
35
+ reads.current.set(key, {
36
+ inputs,
37
+ read
38
+ });
39
+ return read;
40
+ }
41
+ return numbered && text ? strings.copyTitle(text, copy + 1) : text;
42
+ }, [
43
+ engine,
44
+ values,
45
+ strings
46
+ ]);
47
+ const shown = useMemo(() => {
48
+ const order = [];
49
+ const byKey = {};
50
+ for (const step of stepOrder) {
51
+ const stepConfig = steps[step];
52
+ if (!stepConfig) continue;
53
+ if (stepConfig.multi !== true) {
54
+ order.push(step);
55
+ byKey[step] = {
56
+ step,
57
+ config: stepConfig
58
+ };
59
+ continue;
60
+ }
61
+ const rows = getValueUsingPath(values, `${fieldNamePrefix}${step}`);
62
+ const { lo, hi } = copyBounds(stepConfig.min, stepConfig.max);
63
+ const count = Math.min(Math.max(Array.isArray(rows) ? rows.length : 0, lo), hi ?? Infinity);
64
+ if (count === 0) {
65
+ if (hi === 0) continue;
66
+ order.push(step);
67
+ byKey[step] = {
68
+ step,
69
+ count,
70
+ canAdd: hi === void 0 || hi > 0,
71
+ canRemove: false,
72
+ config: {
73
+ ...stepConfig,
74
+ title: perCopy(stepConfig.title, 0, false),
75
+ description: perCopy(stepConfig.description, 0, false),
76
+ options: {}
77
+ }
78
+ };
79
+ continue;
80
+ }
81
+ for (let copy = 0; copy < count; copy++) {
82
+ const key = `${step}[${copy}]`;
83
+ order.push(key);
84
+ byKey[key] = {
85
+ step,
86
+ copy,
87
+ count,
88
+ canAdd: hi === void 0 || count < hi,
89
+ canRemove: count > lo,
90
+ config: {
91
+ ...stepConfig,
92
+ title: perCopy(stepConfig.title, copy, true),
93
+ description: perCopy(stepConfig.description, copy, false)
94
+ }
95
+ };
96
+ }
97
+ }
98
+ return {
99
+ order,
100
+ byKey
101
+ };
102
+ }, [
103
+ stepOrder,
104
+ steps,
105
+ values,
106
+ fieldNamePrefix,
107
+ perCopy
108
+ ]);
109
+ const setAside = useRef({});
110
+ useEffect(() => {
111
+ setAside.current = {};
112
+ }, [initialValues]);
113
+ useEffect(() => {
114
+ for (const step of stepOrder) {
115
+ const stepConfig = steps[step];
116
+ if (stepConfig?.multi !== true) continue;
117
+ const { lo, hi } = copyBounds(stepConfig.min, stepConfig.max);
118
+ const path = `${fieldNamePrefix}${step}`;
119
+ const rows = getValueUsingPath(values, path);
120
+ const now = Array.isArray(rows) ? rows : [];
121
+ setAside.current[path] ??= [];
122
+ const aside = setAside.current[path];
123
+ if (hi !== void 0 && now.length > hi) {
124
+ now.slice(hi).forEach((row, i) => {
125
+ aside[hi + i] = row;
126
+ });
127
+ setFieldValue(path, now.slice(0, hi));
128
+ } else if (now.length < lo) setFieldValue(path, Array.from({ length: lo }, (_, i) => now[i] ?? takeAside(aside, i) ?? initializeValues(stepConfig.options, {}) ?? {}));
129
+ }
130
+ }, [
131
+ stepOrder,
132
+ steps,
133
+ values,
134
+ fieldNamePrefix,
135
+ setFieldValue
136
+ ]);
137
+ const configs = useMemo(() => Object.fromEntries(Object.entries(shown.byKey).map(([key, page]) => [key, page.config])), [shown]);
138
+ const validateCurrentStep = useCallback(async (pageKey) => {
139
+ const page = shown.byKey[pageKey];
140
+ if (!page?.config.options) return true;
141
+ const stepPrefix = page.copy !== void 0 ? `${fieldNamePrefix}${page.step}[${page.copy}].` : config.flatten !== false ? fieldNamePrefix : `${fieldNamePrefix}${page.step}.`;
142
+ const stepFieldPaths = Object.keys(page.config.options).map((name) => `${stepPrefix}${name}`);
20
143
  const errors = await validateForm();
21
- const hasStepErrors = shouldFlatten ? stepFieldNames.some((fieldName) => errors[fieldName]) : stepFieldNames.some((fieldName) => hasNestedError(errors[stepKey], fieldName));
144
+ const hasStepErrors = stepFieldPaths.some((path) => getValueUsingPath(errors, path));
22
145
  if (hasStepErrors) {
23
- const touchedFields = Object.fromEntries(stepFieldNames.map((name) => [name, true]));
24
- if (shouldFlatten) setTouched({
25
- ...touched,
26
- ...touchedFields
27
- }, false);
28
- else setTouched({
29
- ...touched,
30
- [stepKey]: touchedFields
31
- }, false);
146
+ if (stepPrefix) for (const path of stepFieldPaths) setFieldTouched(path, true, false);
147
+ else {
148
+ const touchedFields = Object.fromEntries(stepFieldPaths.map((path) => [path, true]));
149
+ setTouched({
150
+ ...touched,
151
+ ...touchedFields
152
+ }, false);
153
+ }
32
154
  }
33
155
  return !hasStepErrors;
34
156
  }, [
35
- steps,
157
+ shown,
36
158
  config,
159
+ fieldNamePrefix,
37
160
  validateForm,
38
161
  setTouched,
162
+ setFieldTouched,
39
163
  touched
40
164
  ]);
41
- const { currentStep, visitedSteps, invalidSteps, isLastStep, canGoBack, goToNext, goToPrevious, jumpToStep } = useWizardState({
42
- stepOrder,
43
- steps,
165
+ const { currentStep, visitedSteps, invalidSteps, isLastStep, canGoBack, goToNext, goToPrevious, jumpToStep, showStep } = useWizardState({
166
+ stepOrder: shown.order,
167
+ steps: configs,
44
168
  validateStep: validateCurrentStep
45
169
  });
46
- const currentStepConfig = steps[currentStep];
47
170
  const navigation = config.navigation ?? {};
48
171
  const handleGoToNext = useCallback(async () => {
49
172
  const success = await goToNext();
@@ -74,48 +197,120 @@ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className =
74
197
  touched,
75
198
  onSubmit
76
199
  ]);
200
+ const shownBefore = useRef(shown.order);
201
+ useEffect(() => {
202
+ shownBefore.current = shown.order;
203
+ });
204
+ const pageKey = shown.byKey[currentStep] ? currentStep : nearestPage(currentStep, shown.order, shownBefore.current);
205
+ useEffect(() => {
206
+ if (pageKey && pageKey !== currentStep) showStep(pageKey);
207
+ }, [
208
+ pageKey,
209
+ currentStep,
210
+ showStep
211
+ ]);
212
+ const page = shown.byKey[pageKey];
213
+ const copiesOf = (step) => {
214
+ const rows = getValueUsingPath(values, `${fieldNamePrefix}${step}`);
215
+ return Array.isArray(rows) ? rows : [];
216
+ };
217
+ const addCopy = (shownPage) => {
218
+ const options = steps[shownPage.step]?.options;
219
+ const rows = copiesOf(shownPage.step);
220
+ const kept = Array.from({ length: shownPage.count ?? rows.length }, (_, i) => rows[i] ?? initializeValues(options, {}) ?? {});
221
+ const aside = setAside.current[`${fieldNamePrefix}${shownPage.step}`] ?? [];
222
+ setFieldValue(`${fieldNamePrefix}${shownPage.step}`, [...kept, takeAside(aside, kept.length) ?? initializeValues(options, {}) ?? {}]);
223
+ showStep(`${shownPage.step}[${kept.length}]`);
224
+ };
225
+ const removeCopy = (shownPage) => {
226
+ const copy = shownPage.copy ?? 0;
227
+ const rows = copiesOf(shownPage.step).filter((_, i) => i !== copy);
228
+ setFieldValue(`${fieldNamePrefix}${shownPage.step}`, rows);
229
+ showStep(rows.length > 0 ? `${shownPage.step}[${Math.max(copy - 1, 0)}]` : shownPage.step);
230
+ };
231
+ const rawTitle = page ? steps[page.step]?.title : void 0;
232
+ const titledPerCopy = Array.isArray(rawTitle) || typeof rawTitle === "string" && rawTitle.includes("${{");
233
+ const removeTitle = titledPerCopy && page ? stepText(page.config.title) : stepText(rawTitle);
234
+ const addTitle = titledPerCopy && page?.count !== void 0 ? perCopy(rawTitle, page.count, true) : stepText(rawTitle);
235
+ const copies = page?.count !== void 0 && (page.canAdd || page.canRemove) ? /* @__PURE__ */ jsxs("div", {
236
+ className: "mb-4 flex items-center justify-between gap-3",
237
+ children: [page.canRemove ? /* @__PURE__ */ jsx("button", {
238
+ type: "button",
239
+ onClick: () => removeCopy(page),
240
+ className: "rounded-lg border theme-border px-4 py-2 text-sm theme-muted-text hover:text-(--theme-app)",
241
+ children: strings.removeCopy(removeTitle)
242
+ }) : /* @__PURE__ */ jsx("span", {}), page.canAdd && (page.copy === void 0 || page.copy === page.count - 1) && /* @__PURE__ */ jsx("button", {
243
+ type: "button",
244
+ onClick: () => addCopy(page),
245
+ className: "rounded-lg bg-(--theme-element) px-4 py-2 font-medium text-(--theme-element-text) text-sm",
246
+ children: strings.addCopy(addTitle)
247
+ })]
248
+ }) : null;
249
+ const content = page ? /* @__PURE__ */ jsxs("div", {
250
+ className: "mb-4",
251
+ children: [/* @__PURE__ */ jsx(WizardStepContent, {
252
+ currentStep: page.step,
253
+ stepConfig: page.config,
254
+ flatten: config.flatten,
255
+ values,
256
+ labelPosition,
257
+ missingFields,
258
+ spaceCompact,
259
+ setFormDirty: () => onChange?.(values),
260
+ workflowForm,
261
+ fieldNamePrefix,
262
+ copy: page.copy,
263
+ setFieldValue,
264
+ setFieldTouched
265
+ }), copies]
266
+ }) : null;
77
267
  return /* @__PURE__ */ jsxs("div", {
78
268
  className,
79
269
  children: [
80
270
  navigation.showSteps !== false && /* @__PURE__ */ jsx(WizardStepIndicator, {
81
- stepOrder,
82
- currentStep,
83
- steps,
271
+ stepOrder: shown.order,
272
+ currentStep: pageKey,
273
+ steps: configs,
84
274
  visitedSteps,
85
275
  invalidSteps,
86
276
  onStepClick: jumpToStep,
87
277
  allowJump: navigation.allowJump,
88
278
  hideStepNumbers: navigation.hideStepNumbers
89
279
  }),
90
- currentStepConfig && /* @__PURE__ */ jsx("div", {
91
- className: "mb-4",
92
- children: /* @__PURE__ */ jsx(WizardStepContent, {
93
- currentStep,
94
- stepConfig: currentStepConfig,
95
- flatten: config.flatten,
96
- values,
97
- labelPosition,
98
- missingFields,
99
- spaceCompact,
100
- setFormDirty: () => onChange?.(values),
101
- workflowForm,
102
- setFieldValue,
103
- setFieldTouched
104
- })
105
- }),
280
+ content,
106
281
  /* @__PURE__ */ jsx(WizardNavigation, {
107
282
  isLastStep,
108
283
  canGoBack,
109
- isCurrentStepValid: !invalidSteps.has(currentStep),
110
- nextLabel: currentStepConfig?.nextLabel,
111
- prevLabel: currentStepConfig?.prevLabel,
284
+ isCurrentStepValid: !invalidSteps.has(pageKey),
285
+ nextLabel: page?.config.nextLabel,
286
+ prevLabel: page?.config.prevLabel,
112
287
  submitLabel: config.submitLabel || "Execute",
113
288
  onNext: handleGoToNext,
114
289
  onPrevious: goToPrevious,
115
- onSubmit: handleSubmit
290
+ onSubmit: nested ? void 0 : handleSubmit
116
291
  })
117
292
  ]
118
293
  });
119
294
  }
295
+ function nearestPage(key, order, before) {
296
+ const [, step = key, index = "0"] = /^(.*)\[(\d+)\]$/.exec(key) ?? [];
297
+ const copies = order.filter((other) => other.startsWith(`${step}[`));
298
+ if (copies.length > 0) return copies[Math.min(Number(index), copies.length - 1)] ?? copies[0] ?? "";
299
+ if (order.includes(step)) return step;
300
+ const pagesOf = (page) => order.filter((other) => stepOf(other) === stepOf(page));
301
+ const at = before.indexOf(key);
302
+ const next = before.slice(at + 1).find((page) => pagesOf(page).length > 0);
303
+ const last = before.slice(0, Math.max(at, 0)).reverse().find((page) => pagesOf(page).length > 0);
304
+ return (next && pagesOf(next)[0]) ?? (last && pagesOf(last).at(-1)) ?? order[0] ?? "";
305
+ }
306
+ function stepOf(page) {
307
+ return /^(.*)\[\d+\]$/.exec(page)?.[1] ?? page;
308
+ }
309
+ /** The row set aside at `index`, now taken back so it returns only once. */
310
+ function takeAside(aside, index) {
311
+ const row = aside[index];
312
+ delete aside[index];
313
+ return row;
314
+ }
120
315
  //#endregion
121
316
  export { WizardContainer };
@@ -20,7 +20,7 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
20
20
  setIsLoading(true);
21
21
  setError(null);
22
22
  try {
23
- await onSubmit();
23
+ await onSubmit?.();
24
24
  } catch (err) {
25
25
  setError(err instanceof Error ? err.message : "An error occurred");
26
26
  } finally {
@@ -50,11 +50,11 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
50
50
  },
51
51
  "aria-label": "Go to previous step",
52
52
  children: prevLabel || "← Previous"
53
- }), isLastStep ? /* @__PURE__ */ jsxs("button", {
53
+ }), isLastStep && !onSubmit ? null : isLastStep ? /* @__PURE__ */ jsxs("button", {
54
54
  type: "button",
55
55
  onClick: handleSubmit,
56
56
  disabled: !isCurrentStepValid || isLoading,
57
- className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-white flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
57
+ className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-(--theme-element-text) flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
58
58
  style: { backgroundColor: "var(--theme-element)" },
59
59
  onMouseEnter: (e) => {
60
60
  if (isCurrentStepValid && !isLoading) e.currentTarget.style.filter = "brightness(0.9)";
@@ -71,7 +71,7 @@ function WizardNavigation({ isLastStep, canGoBack, isCurrentStepValid, nextLabel
71
71
  type: "button",
72
72
  onClick: handleNext,
73
73
  disabled: isLoading,
74
- className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-white flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
74
+ className: "px-8 py-3 rounded-lg font-medium transition-all duration-200 text-(--theme-element-text) flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg",
75
75
  style: { backgroundColor: "var(--theme-element)" },
76
76
  onMouseEnter: (e) => {
77
77
  if (!isLoading) e.currentTarget.style.filter = "brightness(0.9)";
@@ -1,6 +1,6 @@
1
1
  import type { TSetFormDirty } from '../Form.js';
2
2
  import type { WizardStepContentProps } from './types.js';
3
- export declare function WizardStepContent({ currentStep, stepConfig, flatten, values, setFormDirty, setFieldValue, setFieldTouched, ...fieldsProps }: WizardStepContentProps & {
3
+ export declare function WizardStepContent({ currentStep, stepConfig, flatten, values, setFormDirty, setFieldValue, setFieldTouched, fieldNamePrefix, copy, ...fieldsProps }: WizardStepContentProps & {
4
4
  setFormDirty: TSetFormDirty;
5
5
  setFieldValue: (field: string, value: unknown, shouldValidate?: boolean) => void;
6
6
  setFieldTouched: (field: string, touched?: boolean, shouldValidate?: boolean) => void;
@@ -1,20 +1,32 @@
1
+ import { stepText } from "./utils.js";
1
2
  import { FieldsFromOptions } from "../Form.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/form/Wizard/WizardStepContent.tsx
4
- function WizardStepContent({ currentStep, stepConfig, flatten = true, values, setFormDirty, setFieldValue, setFieldTouched, ...fieldsProps }) {
5
+ function WizardStepContent({ currentStep, stepConfig, flatten = true, values, setFormDirty, setFieldValue, setFieldTouched, fieldNamePrefix = "", copy, ...fieldsProps }) {
5
6
  if (!stepConfig.options) return null;
7
+ const parentInfo = copy !== void 0 ? {
8
+ parentName: currentStep,
9
+ fieldNamePrefix: `${fieldNamePrefix}${currentStep}[${copy}].`,
10
+ arrayIndex: copy
11
+ } : flatten === false ? {
12
+ parentName: currentStep,
13
+ fieldNamePrefix: `${fieldNamePrefix}${currentStep}.`
14
+ } : fieldNamePrefix ? {
15
+ parentName: fieldNamePrefix.slice(0, -1),
16
+ fieldNamePrefix
17
+ } : void 0;
6
18
  return /* @__PURE__ */ jsxs("div", {
7
19
  className: "mb-4 w-full",
8
20
  children: [
9
- stepConfig.title && /* @__PURE__ */ jsx("h2", {
21
+ stepText(stepConfig.title) && /* @__PURE__ */ jsx("h2", {
10
22
  className: "text-2xl font-bold mb-2",
11
23
  style: { color: "var(--theme-app)" },
12
- children: stepConfig.title
24
+ children: stepText(stepConfig.title)
13
25
  }),
14
- stepConfig.description && /* @__PURE__ */ jsx("p", {
26
+ stepText(stepConfig.description) && /* @__PURE__ */ jsx("p", {
15
27
  className: "mb-4 leading-relaxed",
16
28
  style: { color: "var(--theme-muted-text-color)" },
17
- children: stepConfig.description
29
+ children: stepText(stepConfig.description)
18
30
  }),
19
31
  /* @__PURE__ */ jsx(FieldsFromOptions, {
20
32
  options: stepConfig.options,
@@ -22,12 +34,9 @@ function WizardStepContent({ currentStep, stepConfig, flatten = true, values, se
22
34
  setFormDirty,
23
35
  setFieldValue,
24
36
  setFieldTouched,
25
- ...flatten === false ? { parentInfo: {
26
- parentName: currentStep,
27
- fieldNamePrefix: `${currentStep}.`
28
- } } : {},
37
+ ...parentInfo ? { parentInfo } : {},
29
38
  ...fieldsProps
30
- }, currentStep)
39
+ }, copy === void 0 ? currentStep : `${currentStep}[${copy}]`)
31
40
  ]
32
41
  });
33
42
  }
@@ -1,15 +1,42 @@
1
+ import { useStrings } from "../../components/Provider.js";
2
+ import { stepText } from "./utils.js";
1
3
  import cx from "classnames";
4
+ import { useEffect, useRef, useState } from "react";
2
5
  import { jsx, jsxs } from "react/jsx-runtime";
3
6
  //#region src/form/Wizard/WizardStepIndicator.tsx
4
7
  function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, invalidSteps, onStepClick, allowJump = false, hideStepNumbers = false }) {
8
+ const { form: strings } = useStrings();
5
9
  const currentIndex = stepOrder.indexOf(currentStep);
10
+ const row = useRef(null);
11
+ const [scrolls, setScrolls] = useState(false);
12
+ useEffect(() => {
13
+ const el = row.current;
14
+ if (!el) return;
15
+ const measure = () => setScrolls(el.scrollWidth > el.clientWidth);
16
+ measure();
17
+ if (typeof ResizeObserver === "undefined") return;
18
+ const observer = new ResizeObserver(measure);
19
+ for (const box of [el, ...el.children]) observer.observe(box);
20
+ return () => observer.disconnect();
21
+ }, [stepOrder.length]);
22
+ useEffect(() => {
23
+ const el = row.current;
24
+ const step = el?.children[currentIndex];
25
+ if (!el || !step || el.scrollWidth <= el.clientWidth) return;
26
+ const box = el.getBoundingClientRect();
27
+ const at = step.getBoundingClientRect();
28
+ if (at.left < box.left || at.right > box.right) el.scrollLeft += at.left - box.left - (box.width - at.width) / 2;
29
+ }, [currentIndex]);
6
30
  const getStepStatus = (stepKey, index) => {
7
31
  if (stepKey === currentStep) return "active";
8
32
  if (index < currentIndex) return "completed";
9
33
  return "upcoming";
10
34
  };
11
- return /* @__PURE__ */ jsx("div", {
12
- className: "flex items-start justify-between w-full mb-2 gap-4 py-2",
35
+ return /* @__PURE__ */ jsx("ol", {
36
+ ref: row,
37
+ "aria-label": strings.steps,
38
+ tabIndex: scrolls ? 0 : void 0,
39
+ className: "flex items-start justify-between w-full mb-2 gap-4 py-2 overflow-x-auto",
13
40
  children: stepOrder.map((stepKey, index) => {
14
41
  const stepConfig = steps[stepKey];
15
42
  const status = getStepStatus(stepKey, index);
@@ -17,13 +44,14 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
17
44
  const isClickable = allowJump && visitedSteps.has(stepKey) && stepKey !== currentStep;
18
45
  const isDotActive = status === "active";
19
46
  const isDotCompleted = status === "completed";
20
- return /* @__PURE__ */ jsxs("div", {
47
+ return /* @__PURE__ */ jsxs("li", {
21
48
  className: "flex flex-col items-center flex-1 relative",
22
49
  children: [
23
50
  /* @__PURE__ */ jsxs("button", {
24
51
  type: "button",
25
52
  onClick: () => onStepClick(stepKey),
26
53
  disabled: !isClickable,
54
+ "aria-current": isDotActive ? "step" : void 0,
27
55
  className: cx("w-12 h-12 rounded-full border-3 flex items-center justify-center font-bold text-base transition-all duration-300 flex-shrink-0 relative", {
28
56
  "cursor-pointer": isClickable,
29
57
  "cursor-not-allowed": !isClickable
@@ -40,8 +68,8 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
40
68
  boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
41
69
  zIndex: 10
42
70
  } : { zIndex: 10 },
43
- "aria-label": `Step ${index + 1}: ${stepConfig?.title}`,
44
- title: stepConfig?.title,
71
+ "aria-label": `Step ${index + 1}: ${stepText(stepConfig?.title)}`,
72
+ title: stepText(stepConfig?.title),
45
73
  children: [!hideStepNumbers && /* @__PURE__ */ jsx("span", {
46
74
  className: cx({ hidden: status === "completed" }),
47
75
  children: index + 1
@@ -83,10 +111,10 @@ function WizardStepIndicator({ stepOrder, currentStep, steps, visitedSteps, inva
83
111
  "font-bold text-white": isDotActive
84
112
  }),
85
113
  style: isDotActive ? { color: "var(--theme-element)" } : {},
86
- children: stepConfig?.title
114
+ children: stepText(stepConfig?.title)
87
115
  }), stepConfig?.description && /* @__PURE__ */ jsx("div", {
88
116
  className: "text-xs theme-muted-text mt-1 text-center max-w-30",
89
- children: stepConfig.description
117
+ children: stepText(stepConfig.description)
90
118
  })]
91
119
  })
92
120
  ]
@@ -23,10 +23,10 @@ export interface WizardConfig {
23
23
  export interface StepFieldConfig {
24
24
  /** Must be 'step' */
25
25
  type: 'step';
26
- /** Title shown in step indicator */
27
- title: string;
28
- /** Optional description/subtitle shown below title */
29
- description?: string;
26
+ /** Title shown in step indicator; a repeated page's can be one per copy */
27
+ title: string | string[];
28
+ /** Optional description/subtitle shown below title; a repeated page's can be one per copy */
29
+ description?: string | string[];
30
30
  /** Schema for fields within this step */
31
31
  options: DynamicFormSchema;
32
32
  /** Validate current step before proceeding to next (default: true) */
@@ -35,6 +35,11 @@ export interface StepFieldConfig {
35
35
  nextLabel?: string;
36
36
  /** Custom label for previous button on this step */
37
37
  prevLabel?: string;
38
+ /** The person running it can add copies of this page; its values are a list, one row per copy */
39
+ multi?: boolean;
40
+ /** The fewest and most copies of a repeated page, as a list's rows: numbers or expressions */
41
+ min?: number | string;
42
+ max?: number | string;
38
43
  }
39
44
  /**
40
45
  * Extracted wizard configuration and step information
@@ -69,6 +74,10 @@ export interface WizardContainerProps {
69
74
  spaceCompact?: boolean;
70
75
  /** Workflow form mode */
71
76
  workflowForm?: boolean;
77
+ /** A wizard inside a group submits with the form around it, so its last step has no submit. */
78
+ nested?: boolean;
79
+ /** Where the fields' values sit when the wizard is inside a group that keeps its own values. */
80
+ fieldNamePrefix?: string | undefined;
72
81
  }
73
82
  /**
74
83
  * Props for WizardStepIndicator component
@@ -111,8 +120,8 @@ export interface WizardNavigationProps {
111
120
  onNext: () => Promise<boolean>;
112
121
  /** Callback to go to previous step */
113
122
  onPrevious: () => void;
114
- /** Callback to submit wizard */
115
- onSubmit: () => Promise<void>;
123
+ /** Callback to submit wizard; without one the last step has no button of its own */
124
+ onSubmit?: (() => Promise<void>) | undefined;
116
125
  }
117
126
  /**
118
127
  * Props for WizardStepContent component
@@ -132,5 +141,9 @@ export interface WizardStepContentProps {
132
141
  spaceCompact?: boolean;
133
142
  /** Workflow form mode */
134
143
  workflowForm?: boolean;
144
+ /** Where the fields' values sit when the wizard is inside a group that keeps its own values. */
145
+ fieldNamePrefix?: string | undefined;
146
+ /** Which copy of a repeated page this is; its fields' values sit in that row of the page's list. */
147
+ copy?: number | undefined;
135
148
  }
136
149
  export {};
@@ -16,5 +16,6 @@ export declare function useWizardState({ stepOrder, steps, validateStep }: UseWi
16
16
  goToNext: () => Promise<boolean>;
17
17
  goToPrevious: () => void;
18
18
  jumpToStep: (stepKey: string) => boolean;
19
+ showStep: (stepKey: string) => void;
19
20
  };
20
21
  export {};
@@ -61,7 +61,11 @@ function useWizardState({ stepOrder, steps, validateStep }) {
61
61
  stepOrder,
62
62
  visitedSteps,
63
63
  currentStep
64
- ])
64
+ ]),
65
+ showStep: useCallback((stepKey) => {
66
+ setCurrentStep(stepKey);
67
+ setVisitedSteps((prev) => /* @__PURE__ */ new Set([...prev, stepKey]));
68
+ }, [])
65
69
  };
66
70
  }
67
71
  //#endregion
@@ -1,2 +1,9 @@
1
1
  import type { ParsedWizardConfig } from './types.js';
2
2
  export declare function parseWizardConfig(options: Record<string, unknown>): ParsedWizardConfig | null;
3
+ /** A step's title or description as one text: a list's first entry, as one copy shows it. */
4
+ export declare function stepText(value: string | string[] | undefined): string;
5
+ /** A repeated page's bounds on its copies, read as a list's `min`/`max`; without a `min` it starts empty. */
6
+ export declare function copyBounds(min: unknown, max: unknown): {
7
+ lo: number;
8
+ hi: number | undefined;
9
+ };
@@ -25,5 +25,22 @@ function parseWizardConfig(options) {
25
25
  stepOrder
26
26
  };
27
27
  }
28
+ /** A step's title or description as one text: a list's first entry, as one copy shows it. */
29
+ function stepText(value) {
30
+ return Array.isArray(value) ? value[0] ?? "" : value ?? "";
31
+ }
32
+ /** A repeated page's bounds on its copies, read as a list's `min`/`max`; without a `min` it starts empty. */
33
+ function copyBounds(min, max) {
34
+ const count = (value) => {
35
+ const n = value === void 0 || value === null || value === "" ? NaN : Number(value);
36
+ return Number.isInteger(n) && n >= 0 ? n : void 0;
37
+ };
38
+ const lo = count(min) ?? 0;
39
+ const hi = count(max);
40
+ return {
41
+ lo,
42
+ hi: hi === void 0 ? void 0 : Math.max(hi, lo)
43
+ };
44
+ }
28
45
  //#endregion
29
- export { parseWizardConfig };
46
+ export { copyBounds, parseWizardConfig, stepText };
@@ -5,6 +5,8 @@ export { type FieldLabelBinding, FieldWrapper } from './FieldWrapper.js';
5
5
  export { collectFieldsWithDefaults, DynamicDefaultsSync, DynamicForm, type DynamicFormProps, FieldsFromOptions, findSelfReferencingFieldNames, GroupHeader, MultiSelectionDropdown, resolveMetaOverrides, type TSetFormDirty, useDynamicDefaultsSync, } from './Form.js';
6
6
  export { type FieldContextValue, FieldProvider, useFieldControlProps, useFieldLabelledByProps, useFieldRequired, } from './fieldContext.js';
7
7
  export { type FieldComponent, FieldRegistryContext, Registry, } from './fieldRegistry.js';
8
+ export { type FormLayoutIssue, type FormLayoutNode, type LayoutBreakpoint, type LayoutGap, type LayoutTracks, type ResponsiveLayout, resolveFormLayout, } from './layout.js';
9
+ export { type LayoutCSSIssue, resolveLayoutCSS, } from './layoutCSS.js';
8
10
  export { enforceOneMustBeTrue, flattenGroups, impureSetValueFromPath, initializeValues, resolvedFlag, } from './lib.js';
9
11
  export { default as DropdownWrapper } from './SpecialTypes/DropdownWrapper.js';
10
12
  export { default as FormikMultiSelectDropdown } from './SpecialTypes/FormikMultiSelectDropdown.js';