refactor(form-schema): extract schema types and schema-driven behaviors to shared package

Moves formBuilder types, formValidation, useConditionalLogic, useFormSteps,
and formatFieldValue from apps/portal/src to packages/form-schema/src.
Adds @form-schema path alias to both apps/portal and apps/app.
Vue field components remain per-app to allow independent visual evolution.
Behavior-neutral: all 35 Vitest tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 23:57:39 +02:00
parent b6a3a17b0a
commit dda60ed5e4
48 changed files with 114 additions and 83 deletions

View File

@@ -39,6 +39,12 @@
],
"@validators": [
"./src/@core/utils/validators"
],
"@form-schema/*": [
"../../packages/form-schema/src/*"
],
"vue": [
"./node_modules/vue"
]
},
"lib": [
@@ -63,7 +69,8 @@
"./src/**/*.vue",
"./themeConfig.ts",
"./auto-imports.d.ts",
"./components.d.ts"
"./components.d.ts",
"../../packages/form-schema/src/**/*"
],
"exclude": [
"./dist",

View File

@@ -110,6 +110,9 @@ export default defineConfig({
import.meta.url,
),
),
"@form-schema": fileURLToPath(
new URL("../../packages/form-schema/src", import.meta.url),
),
},
},
server: {

View File

@@ -434,19 +434,16 @@ declare module 'vue' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly emailValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['emailValidator']>
readonly evaluateConditionalLogic: UnwrapRef<typeof import('./src/composables/useConditionalLogic')['evaluateConditionalLogic']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly extractErrorBody: UnwrapRef<typeof import('./src/composables/useFormDraft')['extractErrorBody']>
readonly extractRetryAfterSeconds: UnwrapRef<typeof import('./src/composables/useFormDraft')['extractRetryAfterSeconds']>
readonly formatDate: UnwrapRef<typeof import('./src/@core/utils/formatters')['formatDate']>
readonly formatDateToMonthShort: UnwrapRef<typeof import('./src/@core/utils/formatters')['formatDateToMonthShort']>
readonly formatFieldValue: UnwrapRef<typeof import('./src/composables/formatFieldValue')['formatFieldValue']>
readonly generateDeviceFingerprint: UnwrapRef<typeof import('./src/utils/deviceFingerprint')['generateDeviceFingerprint']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeviceName: UnwrapRef<typeof import('./src/utils/deviceFingerprint')['getDeviceName']>
readonly getValidatorsForField: UnwrapRef<typeof import('./src/utils/formValidation')['getValidatorsForField']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hexToRgb: UnwrapRef<typeof import('./src/@core/utils/colorConverter')['hexToRgb']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
@@ -456,14 +453,12 @@ declare module 'vue' {
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isEmpty: UnwrapRef<typeof import('./src/@core/utils/helpers')['isEmpty']>
readonly isEmptyArray: UnwrapRef<typeof import('./src/@core/utils/helpers')['isEmptyArray']>
readonly isFieldValueEmpty: UnwrapRef<typeof import('./src/utils/formValidation')['isFieldValueEmpty']>
readonly isNullOrUndefined: UnwrapRef<typeof import('./src/@core/utils/helpers')['isNullOrUndefined']>
readonly isObject: UnwrapRef<typeof import('./src/@core/utils/helpers')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isStepValid: UnwrapRef<typeof import('./src/composables/useFormSteps')['isStepValid']>
readonly isToday: UnwrapRef<typeof import('./src/@core/utils/helpers')['isToday']>
readonly kFormatter: UnwrapRef<typeof import('./src/@core/utils/formatters')['kFormatter']>
readonly lengthValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['lengthValidator']>
@@ -526,7 +521,6 @@ declare module 'vue' {
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly resolveVuetifyTheme: UnwrapRef<typeof import('./src/@core/utils/vuetify')['resolveVuetifyTheme']>
readonly rgbaToHex: UnwrapRef<typeof import('./src/@core/utils/colorConverter')['rgbaToHex']>
readonly runValidators: UnwrapRef<typeof import('./src/utils/formValidation')['runValidators']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
@@ -623,7 +617,6 @@ declare module 'vue' {
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFormDraft: UnwrapRef<typeof import('./src/composables/useFormDraft')['useFormDraft']>
readonly useFormSteps: UnwrapRef<typeof import('./src/composables/useFormSteps')['useFormSteps']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PublicFormSubmissionDuplicate } from '@/types/formBuilder'
import type { PublicFormSubmissionDuplicate } from '@form-schema/types/formBuilder'
const props = defineProps<{
data: PublicFormSubmissionDuplicate | null

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
import { usePublicFormToken } from '@/composables/publicFormInjection'
import type { PublicFormField, PublicFormTimeSlot } from '@/types/formBuilder'
import { getValidatorsForField, runValidators } from '@/utils/formValidation'
import type { PublicFormField, PublicFormTimeSlot } from '@form-schema/types/formBuilder'
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import type { PublicFormField } from '@form-schema/types/formBuilder'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { FieldOption, PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField, runValidators } from '@/utils/formValidation'
import type { FieldOption, PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import type { PublicFormField } from '@form-schema/types/formBuilder'
defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { FieldOption, PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { FieldOption, PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import type { PublicFormField } from '@form-schema/types/formBuilder'
defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { FieldOption, PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { FieldOption, PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -16,9 +16,9 @@ import FieldTagPicker from './FieldTagPicker.vue'
import FieldText from './FieldText.vue'
import FieldTextarea from './FieldTextarea.vue'
import FieldUrl from './FieldUrl.vue'
import { evaluateConditionalLogic } from '@/composables/useConditionalLogic'
import { FormFieldType } from '@/types/formBuilder'
import type { FormFieldDisplayWidth, FormValues, PublicFormField } from '@/types/formBuilder'
import { evaluateConditionalLogic } from '@form-schema/composables/useConditionalLogic'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { FormFieldDisplayWidth, FormValues, PublicFormField } from '@form-schema/types/formBuilder'
const props = defineProps<{
field: PublicFormField

View File

@@ -3,8 +3,8 @@ import draggable from 'vuedraggable'
import { useDisplay } from 'vuetify'
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
import { usePublicFormToken } from '@/composables/publicFormInjection'
import type { PublicFormField, PublicFormSectionOption, SectionPriorityValue } from '@/types/formBuilder'
import { getValidatorsForField, runValidators } from '@/utils/formValidation'
import type { PublicFormField, PublicFormSectionOption, SectionPriorityValue } from '@form-schema/types/formBuilder'
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { FieldOption, PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { FieldOption, PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { AvailableTag, PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { AvailableTag, PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { PublicFormField } from '@/types/formBuilder'
import { getValidatorsForField } from '@/utils/formValidation'
import type { PublicFormField } from '@form-schema/types/formBuilder'
import { getValidatorsForField } from '@form-schema/utils/formValidation'
const props = defineProps<{
field: PublicFormField

View File

@@ -3,16 +3,16 @@ import DuplicateSubmissionHint from './DuplicateSubmissionHint.vue'
import IdentityMatchBanner from './IdentityMatchBanner.vue'
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
import { formatFieldValue } from '@/composables/formatFieldValue'
import type { FormStep } from '@/composables/useFormSteps'
import { formatFieldValue } from '@form-schema/composables/formatFieldValue'
import type { FormStep } from '@form-schema/composables/useFormSteps'
import { usePublicFormToken } from '@/composables/publicFormInjection'
import { FormFieldType } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type {
FormValues,
PublicFormField,
PublicFormSubmissionDuplicate,
PublicFormSubmissionIdentityMatch,
} from '@/types/formBuilder'
} from '@form-schema/types/formBuilder'
const props = defineProps<{
steps: FormStep[]

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { FormErrorCode } from '@/types/formBuilder'
import type { FormErrorCode } from '@form-schema/types/formBuilder'
const props = defineProps<{
errorCode?: FormErrorCode | string

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useDisplay } from 'vuetify'
import type { FormStep } from '@/composables/useFormSteps'
import type { FormStep } from '@form-schema/composables/useFormSteps'
const props = defineProps<{
steps: FormStep[]

View File

@@ -9,7 +9,7 @@ import type {
SaveDraftBody,
StartDraftBody,
SubmitBody,
} from '@/types/formBuilder'
} from '@form-schema/types/formBuilder'
interface ApiResponse<T> {
data: T

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { apiClient } from '@/lib/axios'
import type { PublicFormSectionOption } from '@/types/formBuilder'
import type { PublicFormSectionOption } from '@form-schema/types/formBuilder'
interface ApiResponse<T> {
data: T

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { apiClient } from '@/lib/axios'
import type { PublicFormTimeSlot } from '@/types/formBuilder'
import type { PublicFormTimeSlot } from '@form-schema/types/formBuilder'
interface ApiResponse<T> {
data: T

View File

@@ -7,7 +7,7 @@ import {
useSaveFormDraft,
useSubmitForm,
} from '@/composables/api/usePublicForm'
import type { FormValues, PublicFormSubmission, SaveDraftBody } from '@/types/formBuilder'
import type { FormValues, PublicFormSubmission, SaveDraftBody } from '@form-schema/types/formBuilder'
/** sessionStorage key for reusing an idempotency key across reloads. */
export function draftIdempotencyKey(token: string): string {

View File

@@ -9,11 +9,11 @@ import { extractErrorBody, useFetchPublicFormSchema } from '@/composables/api/us
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
import { useFormDraft } from '@/composables/useFormDraft'
import { isStepValid, useFormSteps } from '@/composables/useFormSteps'
import { formatFieldValue } from '@/composables/formatFieldValue'
import { isStepValid, useFormSteps } from '@form-schema/composables/useFormSteps'
import { formatFieldValue } from '@form-schema/composables/formatFieldValue'
import { providePublicFormToken } from '@/composables/publicFormInjection'
import { FormFieldType } from '@/types/formBuilder'
import type { FormErrorCode, PublicFormField } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { FormErrorCode, PublicFormField } from '@form-schema/types/formBuilder'
definePage({
name: 'public-form-register',

View File

@@ -21,8 +21,8 @@ vi.mock('@/composables/publicFormInjection', () => ({
}))
import FieldAvailabilityPicker from '@/components/public-form/FieldAvailabilityPicker.vue'
import { FormFieldType } from '@/types/formBuilder'
import type { PublicFormField, PublicFormTimeSlot } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { PublicFormField, PublicFormTimeSlot } from '@form-schema/types/formBuilder'
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
return {

View File

@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import FieldRenderer from '@/components/public-form/FieldRenderer.vue'
import { FormFieldType } from '@/types/formBuilder'
import type { PublicFormField } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { PublicFormField } from '@form-schema/types/formBuilder'
function makeField(partial: Partial<PublicFormField>): PublicFormField {
return {

View File

@@ -36,8 +36,8 @@ vi.mock('vuedraggable', () => ({
}))
import FieldSectionPriority from '@/components/public-form/FieldSectionPriority.vue'
import { FormFieldType } from '@/types/formBuilder'
import type { PublicFormField, PublicFormSectionOption } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { PublicFormField, PublicFormSectionOption } from '@form-schema/types/formBuilder'
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
return {

View File

@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import FieldTagPicker from '@/components/public-form/FieldTagPicker.vue'
import { FormFieldType } from '@/types/formBuilder'
import type { AvailableTag, PublicFormField } from '@/types/formBuilder'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type { AvailableTag, PublicFormField } from '@form-schema/types/formBuilder'
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
return {

View File

@@ -9,7 +9,7 @@ vi.mock('@/lib/axios', () => ({
import { apiClient } from '@/lib/axios'
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
import type { PublicFormSectionOption } from '@/types/formBuilder'
import type { PublicFormSectionOption } from '@form-schema/types/formBuilder'
interface MockedApi { get: ReturnType<typeof vi.fn> }
const mocked = apiClient as unknown as MockedApi

View File

@@ -9,7 +9,7 @@ vi.mock('@/lib/axios', () => ({
import { apiClient } from '@/lib/axios'
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
import type { PublicFormTimeSlot } from '@/types/formBuilder'
import type { PublicFormTimeSlot } from '@form-schema/types/formBuilder'
interface MockedApi { get: ReturnType<typeof vi.fn> }
const mocked = apiClient as unknown as MockedApi

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { evaluateConditionalLogic } from '@/composables/useConditionalLogic'
import type { ConditionalLogic } from '@/types/formBuilder'
import { evaluateConditionalLogic } from '@form-schema/composables/useConditionalLogic'
import type { ConditionalLogic } from '@form-schema/types/formBuilder'
describe('evaluateConditionalLogic', () => {
it('returns true when logic is null or undefined', () => {

View File

@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import DuplicateSubmissionHint from '@/components/public-form/DuplicateSubmissionHint.vue'
import type { PublicFormSubmissionDuplicate } from '@/types/formBuilder'
import type { PublicFormSubmissionDuplicate } from '@form-schema/types/formBuilder'
function mountHint(data: PublicFormSubmissionDuplicate | null) {
return mount(DuplicateSubmissionHint, {

View File

@@ -1,12 +1,12 @@
import { describe, expect, it } from 'vitest'
import { formatFieldValue } from '@/composables/formatFieldValue'
import { FormFieldType } from '@/types/formBuilder'
import { formatFieldValue } from '@form-schema/composables/formatFieldValue'
import { FormFieldType } from '@form-schema/types/formBuilder'
import type {
AvailableTag,
PublicFormField,
PublicFormSectionOption,
PublicFormTimeSlot,
} from '@/types/formBuilder'
} from '@form-schema/types/formBuilder'
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
return {

View File

@@ -40,6 +40,12 @@
"@validators": [
"./src/@core/utils/validators"
],
"@form-schema/*": [
"../../packages/form-schema/src/*"
],
"vue": [
"./node_modules/vue"
],
},
"lib": [
"esnext",
@@ -63,7 +69,8 @@
"./src/**/*.vue",
"./themeConfig.ts",
"./auto-imports.d.ts",
"./components.d.ts"
"./components.d.ts",
"../../packages/form-schema/src/**/*"
],
"exclude": [
"./dist",

View File

@@ -84,6 +84,7 @@ export default defineConfig({
'@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)),
'@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)),
'@configured-variables': fileURLToPath(new URL('./src/assets/styles/variables/_template.scss', import.meta.url)),
'@form-schema': fileURLToPath(new URL('../../packages/form-schema/src', import.meta.url)),
},
},
server: {

View File

@@ -23,6 +23,7 @@ export default defineConfig({
'@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)),
'@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)),
'@validators': fileURLToPath(new URL('./src/@core/utils/validators', import.meta.url)),
'@form-schema': fileURLToPath(new URL('../../packages/form-schema/src', import.meta.url)),
},
},
test: {

View File

@@ -0,0 +1,19 @@
# @form-schema
Shared schema contract and schema-driven behaviors for Crewli form rendering. Consumed by apps/portal (public submission) and apps/app (organizer builder + submissions review).
## What lives here
- `types/` — TypeScript types and enum constants mirroring backend `FormBuilder` enums and `PublicForm(Schema|Submission)Resource`
- `utils/formValidation.ts` — validation rule builders driven by `FormFieldValidationRules`
- `composables/useConditionalLogic.ts` — evaluate `conditional_logic.show_when` against field values
- `composables/useFormSteps.ts` — step navigation logic for multi-step forms
- `composables/formatFieldValue.ts` — render a stored submission value as a human-readable string
## What does NOT live here
Vue components. Each app renders its own UI. Portal has full-fidelity submit components; app has builder-preview and submissions-review components. Sharing renderers would couple the two apps' visual styles, which we explicitly want to avoid.
## Contract stability
This is an alias-only shared directory inside the monorepo — no npm package, no semver. Breaking changes (new field type, new validation key, new conditional operator) require updating both apps in the same PR. TypeScript will flag missing cases in dispatchers.

View File

@@ -1,10 +1,10 @@
import { FormFieldType } from '@/types/formBuilder'
import { FormFieldType } from '../types/formBuilder'
import type {
PublicFormField,
PublicFormSectionOption,
PublicFormTimeSlot,
SectionPriorityValue,
} from '@/types/formBuilder'
} from '../types/formBuilder'
const EMPTY = '—'
const LOADING = 'Laden…'

View File

@@ -4,7 +4,7 @@ import type {
ConditionalOperator,
ConditionalRule,
FormValues,
} from '@/types/formBuilder'
} from '../types/formBuilder'
function isEmptyValue(v: unknown): boolean {
if (v === null || v === undefined) return true

View File

@@ -1,9 +1,9 @@
import { computed } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { evaluateConditionalLogic } from '@/composables/useConditionalLogic'
import { FormFieldType } from '@/types/formBuilder'
import type { FormValues, PublicFormField, PublicFormSchema } from '@/types/formBuilder'
import { isFieldValueEmpty } from '@/utils/formValidation'
import { evaluateConditionalLogic } from './useConditionalLogic'
import { FormFieldType } from '../types/formBuilder'
import type { FormValues, PublicFormField, PublicFormSchema } from '../types/formBuilder'
import { isFieldValueEmpty } from '../utils/formValidation'
export type StepKind = 'submitter' | 'section' | 'heading_group' | 'flat' | 'review'

View File

@@ -1,6 +1,6 @@
import { emailValidator, regexValidator, requiredValidator, urlValidator } from '@core/utils/validators'
import { FormFieldType } from '@/types/formBuilder'
import type { PublicFormField } from '@/types/formBuilder'
import { FormFieldType } from '../types/formBuilder'
import type { PublicFormField } from '../types/formBuilder'
export type Validator = (value: unknown) => true | string