@bagelink/vue 1.15.482 → 1.15.486

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.482",
4
+ "version": "1.15.486",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -400,6 +400,7 @@ function getFieldProps(field: FieldBuilder, key: string) {
400
400
  'onUpdate:modelValue': (val: any) => { updateField(key, val, field) },
401
401
  'error': mergedErrors.value[key],
402
402
  ...configRest,
403
+ 'formData': formData.value,
403
404
  // Inputs use `helptext`; FormFlow schema API keeps `helpText`
404
405
  ...(helpText != null ? { helptext: helpText } : {}),
405
406
  name: field._config.name ?? key,
@@ -23,6 +23,9 @@ import { normalizeCustomFormSubmission } from './custom-field-registry'
23
23
  import { fromJSONSchema } from './form-flow'
24
24
  import type { JSONSchemaOptionsSourceLoader } from './form-flow'
25
25
  import Btn from '../components/Btn.vue'
26
+ import Icon from '../components/Icon/Icon.vue'
27
+ import ListItem from '../components/ListItem.vue'
28
+ import Progress from '../components/Progress.vue'
26
29
  import FormFlow from './FormFlow.vue'
27
30
  import type { FormStepCompleteEventPayload, FormStepEventPayload } from './structure'
28
31
  import { autoAdvanceFieldKey, entryStepKey, pathProgress, resolveNext, stepAutoAdvances, stepByKey } from './structure'
@@ -31,6 +34,8 @@ export interface Props {
31
34
  structure: FormStructure
32
35
  modelValue?: Record<string, any>
33
36
  components?: Record<string, any>
37
+ /** Stable step key to resume. Falls back to the structure entry step. */
38
+ initialStepKey?: string | null
34
39
  /** Human name of the form, included in step/complete event payloads. */
35
40
  formName?: string
36
41
  /** Slide between steps (honors prefers-reduced-motion). Default true. */
@@ -52,6 +57,8 @@ export interface Props {
52
57
  hideTitle?: boolean
53
58
  /** Hide the step-position counter pill in the default nav. Default false. */
54
59
  hideCounter?: boolean
60
+ /** Show numbered stages with completed-step navigation. Default false. */
61
+ showStages?: boolean
55
62
  /** Optional async submit handler; awaited before `complete` is emitted. */
56
63
  onComplete?: (data: Record<string, any>) => Promise<void> | void
57
64
  /**
@@ -75,6 +82,7 @@ const props = withDefaults(defineProps<Props>(), {
75
82
  redirect: true,
76
83
  hideTitle: false,
77
84
  hideCounter: false,
85
+ showStages: false,
78
86
  })
79
87
 
80
88
  const emit = defineEmits<{
@@ -116,14 +124,30 @@ watch(() => props.modelValue, (val) => {
116
124
 
117
125
  // Navigation state: the current step key plus the stack of previously visited
118
126
  // keys (for Back). Initialised to the structure's entry step.
119
- const currentKey = ref<string | undefined>(entryStepKey(props.structure))
127
+ function initialKey(): string | undefined {
128
+ const requested = props.initialStepKey
129
+ return requested && stepByKey(props.structure, requested)
130
+ ? requested
131
+ : entryStepKey(props.structure)
132
+ }
133
+
134
+ const currentKey = ref<string | undefined>(initialKey())
120
135
  const history = ref<string[]>([])
136
+ const completedKeys = ref<Set<string>>(new Set())
121
137
 
122
138
  watch(() => props.structure, () => {
123
- currentKey.value = entryStepKey(props.structure)
139
+ currentKey.value = initialKey()
124
140
  history.value = []
141
+ completedKeys.value = new Set()
125
142
  }, { deep: true })
126
143
 
144
+ watch(() => props.initialStepKey, (key) => {
145
+ if (!key || key === currentKey.value || !stepByKey(props.structure, key)) return
146
+ currentKey.value = key
147
+ history.value = []
148
+ completedKeys.value = new Set()
149
+ })
150
+
127
151
  watch(currentKey, (key) => {
128
152
  if (key) emit('stepChange', key)
129
153
  }, { immediate: true })
@@ -188,6 +212,32 @@ const canGoBack = computed(() => history.value.length > 0)
188
212
  // host can render its own <Progress> (the player stays chrome-light).
189
213
  const progress = computed(() => pathProgress(props.structure, currentKey.value, formData.value))
190
214
  watch(progress, val => emit('progress', val), { immediate: true })
215
+ const pathSteps = computed(() => {
216
+ const result: FormStep[] = []
217
+ const seen = new Set<string>()
218
+ let key = entryStepKey(props.structure)
219
+ while (key && !seen.has(key)) {
220
+ seen.add(key)
221
+ const step = stepByKey(props.structure, key)
222
+ if (!step) break
223
+ result.push(step)
224
+ key = resolveNext(props.structure, key, formData.value) ?? undefined
225
+ }
226
+ return result
227
+ })
228
+ const currentPathIndex = computed(() =>
229
+ pathSteps.value.findIndex(step => step.key === currentKey.value)
230
+ )
231
+ watch([pathSteps, currentPathIndex], ([steps, index]) => {
232
+ if (index <= 0) return
233
+ if (history.value.length === 0) {
234
+ history.value = steps.slice(0, index).map(step => step.key)
235
+ }
236
+ completedKeys.value = new Set([
237
+ ...completedKeys.value,
238
+ ...steps.slice(0, index).map(step => step.key),
239
+ ])
240
+ }, { immediate: true })
191
241
 
192
242
  const formRef = ref<{ validate: () => boolean } | null>(null)
193
243
  const submitting = ref(false)
@@ -243,6 +293,7 @@ async function onPrimary() {
243
293
  // Advance to the resolved next step.
244
294
  const target = nextKey.value
245
295
  if (target && currentKey.value) {
296
+ completedKeys.value = new Set([...completedKeys.value, currentKey.value])
246
297
  const destination = stepByKey(props.structure, target)
247
298
  emit('stepComplete', {
248
299
  ...buildPayload(currentKey.value),
@@ -310,15 +361,68 @@ function onBack() {
310
361
  }
311
362
  }
312
363
 
364
+ function canOpenStage(key: string): boolean {
365
+ if (key === currentKey.value || completedKeys.value.has(key)) return true
366
+ return key === nextKey.value
367
+ }
368
+
369
+ async function goToStage(key: string) {
370
+ if (key === currentKey.value || !canOpenStage(key)) return
371
+ const targetIndex = pathSteps.value.findIndex(step => step.key === key)
372
+ const currentIndex = currentPathIndex.value
373
+ if (targetIndex < 0 || currentIndex < 0) return
374
+ if (key === nextKey.value) {
375
+ await onPrimary()
376
+ return
377
+ }
378
+ slideDir.value = targetIndex < currentIndex ? 'prev' : 'next'
379
+ history.value = pathSteps.value.slice(0, targetIndex).map(step => step.key)
380
+ currentKey.value = key
381
+ }
382
+
313
383
  // Progress: position in the visited path (best-effort; branches make total
314
384
  // step count indeterminate, so we show visited-count rather than a fixed total).
315
385
  const stepPosition = computed(() => history.value.length + 1)
316
386
 
317
- defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep })
387
+ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, completedKeys, validateStep })
318
388
  </script>
319
389
 
320
390
  <template>
321
- <div class="form-flow-player">
391
+ <div class="form-flow-player" :class="{ 'form-flow-player--stages': showStages }">
392
+ <nav v-if="showStages" class="ffp-stages" aria-label="Form stages">
393
+ <ol class="ffp-stage-list p-0 m-0">
394
+ <li v-for="(step, index) in pathSteps" :key="step.key">
395
+ <ListItem
396
+ rounded
397
+ color="purple"
398
+ :title="step.title"
399
+ :active="currentKey === step.key"
400
+ :disabled="!canOpenStage(step.key)"
401
+ :aria-current="currentKey === step.key ? 'step' : undefined"
402
+ @click="goToStage(step.key)"
403
+ >
404
+ <template #media>
405
+ <span
406
+ class="ffp-stage-number flex-center round border"
407
+ :class="currentKey === step.key
408
+ ? 'bg-primary color-white border-transparent'
409
+ : completedKeys.has(step.key)
410
+ ? 'bg-green color-white border-green'
411
+ : 'color-gray'"
412
+ >
413
+ <Icon v-if="completedKeys.has(step.key) && currentKey !== step.key" icon="check" :size="0.9" />
414
+ <template v-else>{{ index + 1 }}</template>
415
+ </span>
416
+ </template>
417
+ </ListItem>
418
+ </li>
419
+ </ol>
420
+ <div class="ffp-stage-progress">
421
+ <span>{{ currentStep?.title }}</span>
422
+ <span>{{ currentPathIndex + 1 }} / {{ pathSteps.length }}</span>
423
+ <Progress class="w-100p mt-05" :value="progress" :size="8" />
424
+ </div>
425
+ </nav>
322
426
  <div class="ffp-stage">
323
427
  <Transition :name="transitionName" mode="out-in">
324
428
  <div v-if="currentStep" :key="currentKey" class="ffp-step">
@@ -425,6 +529,29 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
425
529
  gap: 1.5rem;
426
530
  }
427
531
 
532
+ .form-flow-player--stages {
533
+ display: grid;
534
+ grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
535
+ }
536
+
537
+ .ffp-stage-list {
538
+ list-style: none;
539
+ }
540
+
541
+ .ffp-stage-number {
542
+ width: 2rem;
543
+ height: 2rem;
544
+ }
545
+
546
+ .ffp-stage-progress {
547
+ display: none;
548
+ }
549
+
550
+ .form-flow-player--stages > .ffp-stage,
551
+ .form-flow-player--stages > .ffp-nav {
552
+ grid-column: 2;
553
+ }
554
+
428
555
  .ffp-title {
429
556
  margin: 0 0 1rem 0;
430
557
  font-size: 1.5rem;
@@ -470,4 +597,21 @@ defineExpose({ formData, currentKey, nextKey, isTerminal, progress, validateStep
470
597
  transform: none;
471
598
  }
472
599
  }
600
+
601
+ @media (max-width: 910px) {
602
+ .form-flow-player {
603
+ display: flex;
604
+ flex-direction: column;
605
+ }
606
+
607
+ .ffp-stage-list {
608
+ display: none;
609
+ }
610
+
611
+ .ffp-stage-progress {
612
+ display: flex;
613
+ flex-wrap: wrap;
614
+ justify-content: space-between;
615
+ }
616
+ }
473
617
  </style>