@businessdash/sdk 0.9.60
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/LICENSE +21 -0
- package/README.md +899 -0
- package/dist/adapters-revalidate.d.ts +44 -0
- package/dist/adapters-revalidate.d.ts.map +1 -0
- package/dist/adapters-revalidate.js +68 -0
- package/dist/adapters-revalidate.js.map +1 -0
- package/dist/alpha.d.ts +21 -0
- package/dist/alpha.d.ts.map +1 -0
- package/dist/alpha.js +21 -0
- package/dist/alpha.js.map +1 -0
- package/dist/analytics-core.d.ts +34 -0
- package/dist/analytics-core.d.ts.map +1 -0
- package/dist/analytics-core.js +138 -0
- package/dist/analytics-core.js.map +1 -0
- package/dist/analytics-server.d.ts +54 -0
- package/dist/analytics-server.d.ts.map +1 -0
- package/dist/analytics-server.js +78 -0
- package/dist/analytics-server.js.map +1 -0
- package/dist/attribution-logo.d.ts +2 -0
- package/dist/attribution-logo.d.ts.map +1 -0
- package/dist/attribution-logo.js +5 -0
- package/dist/attribution-logo.js.map +1 -0
- package/dist/auth-handler.d.ts +92 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +277 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/biab-forms.css +484 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1824 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +1681 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2845 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +13 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +13 -0
- package/dist/collections.js.map +1 -0
- package/dist/contracts.d.ts +14609 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2428 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data-model-schema.d.ts +962 -0
- package/dist/data-model-schema.d.ts.map +1 -0
- package/dist/data-model-schema.js +2295 -0
- package/dist/data-model-schema.js.map +1 -0
- package/dist/distribution.d.ts +38 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +61 -0
- package/dist/distribution.js.map +1 -0
- package/dist/element.d.ts +121 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +276 -0
- package/dist/element.js.map +1 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/enums.js +38 -0
- package/dist/enums.js.map +1 -0
- package/dist/errors.d.ts +195 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +188 -0
- package/dist/errors.js.map +1 -0
- package/dist/forms-core/autocomplete.d.ts +37 -0
- package/dist/forms-core/autocomplete.d.ts.map +1 -0
- package/dist/forms-core/autocomplete.js +82 -0
- package/dist/forms-core/autocomplete.js.map +1 -0
- package/dist/forms-core/business-function.d.ts +61 -0
- package/dist/forms-core/business-function.d.ts.map +1 -0
- package/dist/forms-core/business-function.js +149 -0
- package/dist/forms-core/business-function.js.map +1 -0
- package/dist/forms-core/calculation.d.ts +72 -0
- package/dist/forms-core/calculation.d.ts.map +1 -0
- package/dist/forms-core/calculation.js +103 -0
- package/dist/forms-core/calculation.js.map +1 -0
- package/dist/forms-core/controller.d.ts +249 -0
- package/dist/forms-core/controller.d.ts.map +1 -0
- package/dist/forms-core/controller.js +590 -0
- package/dist/forms-core/controller.js.map +1 -0
- package/dist/forms-core/elements.d.ts +44 -0
- package/dist/forms-core/elements.d.ts.map +1 -0
- package/dist/forms-core/elements.js +68 -0
- package/dist/forms-core/elements.js.map +1 -0
- package/dist/forms-core/email-domains.d.ts +10 -0
- package/dist/forms-core/email-domains.d.ts.map +1 -0
- package/dist/forms-core/email-domains.js +69 -0
- package/dist/forms-core/email-domains.js.map +1 -0
- package/dist/forms-core/file-rules.d.ts +51 -0
- package/dist/forms-core/file-rules.d.ts.map +1 -0
- package/dist/forms-core/file-rules.js +105 -0
- package/dist/forms-core/file-rules.js.map +1 -0
- package/dist/forms-core/formula.d.ts +114 -0
- package/dist/forms-core/formula.d.ts.map +1 -0
- package/dist/forms-core/formula.js +1170 -0
- package/dist/forms-core/formula.js.map +1 -0
- package/dist/forms-core/index.d.ts +24 -0
- package/dist/forms-core/index.d.ts.map +1 -0
- package/dist/forms-core/index.js +28 -0
- package/dist/forms-core/index.js.map +1 -0
- package/dist/forms-core/units.d.ts +49 -0
- package/dist/forms-core/units.d.ts.map +1 -0
- package/dist/forms-core/units.js +255 -0
- package/dist/forms-core/units.js.map +1 -0
- package/dist/forms-core/us-states.d.ts +14 -0
- package/dist/forms-core/us-states.d.ts.map +1 -0
- package/dist/forms-core/us-states.js +66 -0
- package/dist/forms-core/us-states.js.map +1 -0
- package/dist/forms-core/validation.d.ts +266 -0
- package/dist/forms-core/validation.d.ts.map +1 -0
- package/dist/forms-core/validation.js +850 -0
- package/dist/forms-core/validation.js.map +1 -0
- package/dist/forms.d.ts +19 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +29 -0
- package/dist/forms.js.map +1 -0
- package/dist/gallery-types.d.ts +32 -0
- package/dist/gallery-types.d.ts.map +1 -0
- package/dist/gallery-types.js +2 -0
- package/dist/gallery-types.js.map +1 -0
- package/dist/graphql.d.ts +97 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +212 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.cjs +19448 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/marketing-schema.d.ts +216 -0
- package/dist/marketing-schema.d.ts.map +1 -0
- package/dist/marketing-schema.js +200 -0
- package/dist/marketing-schema.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.d.ts +45 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +43 -0
- package/dist/native.js.map +1 -0
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +84 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/parallel-pages.d.ts +102 -0
- package/dist/parallel-pages.d.ts.map +1 -0
- package/dist/parallel-pages.js +108 -0
- package/dist/parallel-pages.js.map +1 -0
- package/dist/payment-icons.d.ts +66 -0
- package/dist/payment-icons.d.ts.map +1 -0
- package/dist/payment-icons.js +160 -0
- package/dist/payment-icons.js.map +1 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +97 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react-analytics.d.ts +39 -0
- package/dist/react-analytics.d.ts.map +1 -0
- package/dist/react-analytics.js +59 -0
- package/dist/react-analytics.js.map +1 -0
- package/dist/react-attribution.d.ts +99 -0
- package/dist/react-attribution.d.ts.map +1 -0
- package/dist/react-attribution.js +52 -0
- package/dist/react-attribution.js.map +1 -0
- package/dist/react-marketing.d.ts +181 -0
- package/dist/react-marketing.d.ts.map +1 -0
- package/dist/react-marketing.js +215 -0
- package/dist/react-marketing.js.map +1 -0
- package/dist/react.d.ts +727 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2418 -0
- package/dist/react.js.map +1 -0
- package/dist/reserved-paths.d.ts +24 -0
- package/dist/reserved-paths.d.ts.map +1 -0
- package/dist/reserved-paths.js +60 -0
- package/dist/reserved-paths.js.map +1 -0
- package/dist/revalidate-verify.d.ts +30 -0
- package/dist/revalidate-verify.d.ts.map +1 -0
- package/dist/revalidate-verify.js +74 -0
- package/dist/revalidate-verify.js.map +1 -0
- package/dist/sdk.d.ts +678 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +801 -0
- package/dist/sdk.js.map +1 -0
- package/dist/seo.d.ts +218 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +309 -0
- package/dist/seo.js.map +1 -0
- package/dist/socials.d.ts +47 -0
- package/dist/socials.d.ts.map +1 -0
- package/dist/socials.js +172 -0
- package/dist/socials.js.map +1 -0
- package/dist/static.d.ts +14 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +14 -0
- package/dist/static.js.map +1 -0
- package/dist/url-pattern.d.ts +47 -0
- package/dist/url-pattern.d.ts.map +1 -0
- package/dist/url-pattern.js +124 -0
- package/dist/url-pattern.js.map +1 -0
- package/dist/vanilla.d.ts +77 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +903 -0
- package/dist/vanilla.js.map +1 -0
- package/openapi.json +13779 -0
- package/package.json +204 -0
- package/src/adapters-revalidate.ts +88 -0
- package/src/alpha.ts +89 -0
- package/src/analytics-core.ts +175 -0
- package/src/analytics-server.ts +99 -0
- package/src/angular/biab-form.component.ts +1443 -0
- package/src/angular/index.ts +26 -0
- package/src/attribution-logo.ts +5 -0
- package/src/auth-handler.ts +352 -0
- package/src/cli.ts +2517 -0
- package/src/client.ts +3578 -0
- package/src/collections.ts +101 -0
- package/src/contracts.ts +3390 -0
- package/src/data-model-schema.ts +4042 -0
- package/src/distribution.ts +73 -0
- package/src/element.ts +321 -0
- package/src/enums.ts +65 -0
- package/src/errors.ts +338 -0
- package/src/forms-core/autocomplete.ts +97 -0
- package/src/forms-core/business-function.ts +209 -0
- package/src/forms-core/calculation.ts +167 -0
- package/src/forms-core/controller.ts +922 -0
- package/src/forms-core/elements.ts +86 -0
- package/src/forms-core/email-domains.ts +68 -0
- package/src/forms-core/file-rules.ts +123 -0
- package/src/forms-core/formula.ts +1339 -0
- package/src/forms-core/index.ts +122 -0
- package/src/forms-core/units.ts +308 -0
- package/src/forms-core/us-states.ts +68 -0
- package/src/forms-core/validation.ts +1037 -0
- package/src/forms.ts +63 -0
- package/src/gallery-types.ts +31 -0
- package/src/graphql.ts +295 -0
- package/src/index.ts +505 -0
- package/src/marketing-schema.ts +395 -0
- package/src/mcp.ts +108 -0
- package/src/native.ts +73 -0
- package/src/next-revalidate.ts +124 -0
- package/src/parallel-pages.ts +192 -0
- package/src/payment-icons.ts +205 -0
- package/src/proxy.ts +173 -0
- package/src/qwik.tsx +1234 -0
- package/src/react-analytics.tsx +76 -0
- package/src/react-attribution.tsx +192 -0
- package/src/react-marketing.tsx +338 -0
- package/src/react.tsx +5495 -0
- package/src/reserved-paths.ts +65 -0
- package/src/revalidate-verify.ts +98 -0
- package/src/sdk.ts +1671 -0
- package/src/seo.ts +488 -0
- package/src/socials.ts +230 -0
- package/src/solid.tsx +1466 -0
- package/src/static.ts +21 -0
- package/src/styles/biab-forms.css +484 -0
- package/src/svelte/BiabForm.svelte +1069 -0
- package/src/svelte/context.ts +57 -0
- package/src/svelte/index.ts +79 -0
- package/src/url-pattern.ts +153 -0
- package/src/vanilla.ts +1290 -0
- package/src/vue/BiabForm.vue +1538 -0
- package/src/vue/index.ts +119 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@businessdash/sdk/svelte — <BiabForm> drop-in (Svelte 5, runes).
|
|
3
|
+
|
|
4
|
+
Renders a BIAB form end-to-end from the headless core
|
|
5
|
+
(`createFormController` in `@businessdash/sdk/forms`). It is a faithful port of
|
|
6
|
+
the dashboard `FormPreview` renderer: per-type field rendering, the
|
|
7
|
+
condition_block / or_condition / input_group / flex_row tree walk,
|
|
8
|
+
multi-step ("consecutive") navigation + progress (bar | steps),
|
|
9
|
+
concurrent-reveal gating, and the schema's render settings
|
|
10
|
+
(animation, org icon). All markup uses the `biab-*` light-DOM class
|
|
11
|
+
convention so the consumer's CSS cascades — no shadow root.
|
|
12
|
+
|
|
13
|
+
Two data paths (consistent with the other framework bindings):
|
|
14
|
+
|
|
15
|
+
• client mode — pass a `BiabClient` (prop or `setBiabClient` context).
|
|
16
|
+
The controller fetches the schema (when only `slug` is given) and
|
|
17
|
+
submits / uploads through the SDK directly. Only safe where the bearer
|
|
18
|
+
key never reaches the browser.
|
|
19
|
+
|
|
20
|
+
• schema + submitAction mode — pass a pre-fetched `schema` (SSR-loaded
|
|
21
|
+
via your server `BiabClient`) plus a `submitAction(payload, opts)` that
|
|
22
|
+
POSTs to your own endpoint (e.g. SvelteKit `/api/biab/forms/[slug]`).
|
|
23
|
+
The bearer key stays in `$lib/server`. This is the recommended
|
|
24
|
+
SvelteKit pattern and the one the starter wires up.
|
|
25
|
+
-->
|
|
26
|
+
<script lang="ts" module>
|
|
27
|
+
import type { BiabClient } from "../sdk.js";
|
|
28
|
+
import type {
|
|
29
|
+
FormCoreField,
|
|
30
|
+
FormSnapshot,
|
|
31
|
+
FormSubmitOptions,
|
|
32
|
+
FormSubmitResult,
|
|
33
|
+
FormSchema,
|
|
34
|
+
} from "../forms-core/index.js";
|
|
35
|
+
|
|
36
|
+
/** A successful submit result (`ok === true`). */
|
|
37
|
+
export type BiabFormSuccess = Extract<FormSubmitResult, { ok: true }>;
|
|
38
|
+
/** A failed submit result (`ok === false`). */
|
|
39
|
+
export type BiabFormFailure = Extract<FormSubmitResult, { ok: false }>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Re-key the working data (by field `id`) to the published output `key`,
|
|
43
|
+
* then POST it to your own endpoint. Returning a {@link FormSubmitResult}
|
|
44
|
+
* lets `<BiabForm>` surface inline server-side field errors; returning
|
|
45
|
+
* void/anything else is treated as a generic success.
|
|
46
|
+
*/
|
|
47
|
+
export type BiabFormSubmitAction = (
|
|
48
|
+
payload: Record<string, unknown>,
|
|
49
|
+
options: FormSubmitOptions,
|
|
50
|
+
) => Promise<FormSubmitResult | void> | FormSubmitResult | void;
|
|
51
|
+
|
|
52
|
+
export interface BiabFormProps {
|
|
53
|
+
/** Form slug — required in client mode (or when `schema` lacks a slug). */
|
|
54
|
+
slug?: string;
|
|
55
|
+
/** Pre-fetched schema (SSR). When present, no client is needed to render. */
|
|
56
|
+
schema?: FormSchema;
|
|
57
|
+
/** SDK client. Falls back to `setBiabClient` context. Omit in schema+action mode. */
|
|
58
|
+
client?: BiabClient;
|
|
59
|
+
/**
|
|
60
|
+
* Submit override. When provided, `<BiabForm>` validates locally, re-keys
|
|
61
|
+
* the payload to output keys, then calls this instead of the SDK — the
|
|
62
|
+
* server-safe SvelteKit path. Takes precedence over `client.forms.submit`.
|
|
63
|
+
*/
|
|
64
|
+
submitAction?: BiabFormSubmitAction;
|
|
65
|
+
/** Submit button label (final step). Default "Submit". */
|
|
66
|
+
submitLabel?: string;
|
|
67
|
+
/** Merged into every submit (e.g. `source`, `metadata`). */
|
|
68
|
+
submitOptions?: FormSubmitOptions;
|
|
69
|
+
/** Server-validates but stores nothing. */
|
|
70
|
+
dryRun?: boolean;
|
|
71
|
+
/** Fired after a successful submit. */
|
|
72
|
+
onSuccess?: (result: BiabFormSuccess) => void;
|
|
73
|
+
/** Fired after a failed submit. */
|
|
74
|
+
onError?: (result: BiabFormFailure) => void;
|
|
75
|
+
/** Convenience: fired with the re-keyed payload on a successful submit. */
|
|
76
|
+
onSubmit?: (payload: Record<string, unknown>) => void;
|
|
77
|
+
/** Extra class on the root `.biab-form`. (`className` alias also accepted.) */
|
|
78
|
+
class?: string;
|
|
79
|
+
/** React-style alias for `class`. */
|
|
80
|
+
className?: string;
|
|
81
|
+
/** Message shown after a successful submit (when the server gives none). */
|
|
82
|
+
successMessage?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Global HTML autofill switch. `false` emits `autocomplete="off"` on every
|
|
85
|
+
* value-bearing input; omit (default) to infer a sensible token per field.
|
|
86
|
+
*/
|
|
87
|
+
autoComplete?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Per-field autofill overrides keyed by field id: a literal token,
|
|
90
|
+
* `true` (infer), or `false` (off). Wins over `autoComplete` and any
|
|
91
|
+
* schema-level token.
|
|
92
|
+
*/
|
|
93
|
+
fieldAutoComplete?: Record<string, string | boolean>;
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<script lang="ts">
|
|
98
|
+
import { onDestroy, untrack } from "svelte";
|
|
99
|
+
|
|
100
|
+
import {
|
|
101
|
+
concurrentRevealCount,
|
|
102
|
+
createFormController,
|
|
103
|
+
evalCondition,
|
|
104
|
+
fetchIconNames,
|
|
105
|
+
filterIconNames,
|
|
106
|
+
iconUrl,
|
|
107
|
+
shouldSearchOptions,
|
|
108
|
+
toSubmissionPayload,
|
|
109
|
+
} from "../forms-core/index.js";
|
|
110
|
+
import {
|
|
111
|
+
type AutocompleteConfig,
|
|
112
|
+
resolveFieldAutocomplete,
|
|
113
|
+
} from "../forms-core/autocomplete.js";
|
|
114
|
+
|
|
115
|
+
import { formStore, getBiabClient } from "./context.js";
|
|
116
|
+
|
|
117
|
+
let {
|
|
118
|
+
slug,
|
|
119
|
+
schema,
|
|
120
|
+
client,
|
|
121
|
+
submitAction,
|
|
122
|
+
submitLabel = "Submit",
|
|
123
|
+
submitOptions,
|
|
124
|
+
dryRun = false,
|
|
125
|
+
onSuccess,
|
|
126
|
+
onError,
|
|
127
|
+
onSubmit,
|
|
128
|
+
class: klass,
|
|
129
|
+
className,
|
|
130
|
+
successMessage = "Thanks — your response was received.",
|
|
131
|
+
autoComplete,
|
|
132
|
+
fieldAutoComplete,
|
|
133
|
+
}: BiabFormProps = $props();
|
|
134
|
+
|
|
135
|
+
// HTML autofill config threaded to every value-bearing input. `enabled` only
|
|
136
|
+
// when the global switch is set; `overrides` only when per-field tokens exist
|
|
137
|
+
// — mirrors the React adapter so unset props fall through to inference.
|
|
138
|
+
const autocompleteConfig = $derived<AutocompleteConfig>({
|
|
139
|
+
...(autoComplete !== undefined ? { enabled: autoComplete } : {}),
|
|
140
|
+
...(fieldAutoComplete ? { overrides: fieldAutoComplete } : {}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// The controller is built ONCE from the initial props (a form's identity —
|
|
144
|
+
// slug / schema / client — doesn't change mid-mount, and re-creating it would
|
|
145
|
+
// discard the visitor's in-progress input). `untrack` reads the init-time prop
|
|
146
|
+
// values without registering a reactive dependency, which also silences
|
|
147
|
+
// Svelte's `state_referenced_locally` hint for this deliberate one-time read.
|
|
148
|
+
const init = untrack(() => ({
|
|
149
|
+
client: client ?? getBiabClient(),
|
|
150
|
+
slug,
|
|
151
|
+
schema,
|
|
152
|
+
submitOptions: { ...submitOptions, ...(dryRun ? { dryRun: true } : {}) },
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
/** True when an SDK client is available (gates SDK-driven file uploads). */
|
|
156
|
+
const hasClient = Boolean(init.client);
|
|
157
|
+
|
|
158
|
+
// ── Build the controller once. It owns ALL runtime state; we just render. ──
|
|
159
|
+
const controller = createFormController({
|
|
160
|
+
client: init.client,
|
|
161
|
+
slug: init.slug,
|
|
162
|
+
schema: init.schema,
|
|
163
|
+
submitOptions: init.submitOptions,
|
|
164
|
+
onSuccess: (result) => {
|
|
165
|
+
onSubmit?.(toSubmissionPayload(controller.snapshot().fields, controller.snapshot().values));
|
|
166
|
+
onSuccess?.(result);
|
|
167
|
+
},
|
|
168
|
+
onError: (result) => onError?.(result),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Kick the load (fetches the schema in client mode, or adopts the SSR one).
|
|
172
|
+
void controller.load();
|
|
173
|
+
onDestroy(() => controller.destroy());
|
|
174
|
+
|
|
175
|
+
// The single reactive source of truth for the markup.
|
|
176
|
+
const snap = formStore(controller);
|
|
177
|
+
|
|
178
|
+
const rootClass = $derived(
|
|
179
|
+
["biab-form", klass, className].filter(Boolean).join(" "),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// ── Submit: route through `submitAction` (server-safe) or the SDK. ─────────
|
|
183
|
+
async function handleSubmit(): Promise<void> {
|
|
184
|
+
if (submitAction) {
|
|
185
|
+
// Local-validate first; the controller publishes errors + a failure result.
|
|
186
|
+
const missing = controller.validate();
|
|
187
|
+
if (missing.length > 0) return;
|
|
188
|
+
const s = controller.snapshot();
|
|
189
|
+
const payload = toSubmissionPayload(s.fields, s.values);
|
|
190
|
+
const opts: FormSubmitOptions = {
|
|
191
|
+
...submitOptions,
|
|
192
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
submittingViaAction = true;
|
|
196
|
+
const res = await submitAction(payload, opts);
|
|
197
|
+
submittingViaAction = false;
|
|
198
|
+
// A returned FormSubmitResult lets us mirror server-side field errors.
|
|
199
|
+
if (res && typeof res === "object" && "ok" in res) {
|
|
200
|
+
actionResult = res;
|
|
201
|
+
if (res.ok) {
|
|
202
|
+
onSubmit?.(payload);
|
|
203
|
+
onSuccess?.(res);
|
|
204
|
+
} else {
|
|
205
|
+
onError?.(res);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// No structured result → treat as success.
|
|
210
|
+
const synthetic: BiabFormSuccess = {
|
|
211
|
+
ok: true,
|
|
212
|
+
status: 200,
|
|
213
|
+
submissionId: "",
|
|
214
|
+
} as BiabFormSuccess;
|
|
215
|
+
actionResult = synthetic;
|
|
216
|
+
onSubmit?.(payload);
|
|
217
|
+
onSuccess?.(synthetic);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
submittingViaAction = false;
|
|
220
|
+
const failure: BiabFormFailure = {
|
|
221
|
+
ok: false,
|
|
222
|
+
status: 0,
|
|
223
|
+
reason: "network_error",
|
|
224
|
+
message:
|
|
225
|
+
err instanceof Error ? err.message : "The submission failed.",
|
|
226
|
+
} as BiabFormFailure;
|
|
227
|
+
actionResult = failure;
|
|
228
|
+
onError?.(failure);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// SDK path: the controller validates + submits and notifies the store.
|
|
233
|
+
await controller.submit(dryRun ? { dryRun: true } : undefined);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `submitAction` mode tracks its own submitting/result locally (the controller
|
|
237
|
+
// never makes the network call in that mode).
|
|
238
|
+
let submittingViaAction = $state(false);
|
|
239
|
+
let actionResult = $state<FormSubmitResult | null>(null);
|
|
240
|
+
|
|
241
|
+
const submitting = $derived($snap.submitting || submittingViaAction);
|
|
242
|
+
const submitted = $derived(
|
|
243
|
+
$snap.submitted || actionResult?.ok === true,
|
|
244
|
+
);
|
|
245
|
+
const submitError = $derived(
|
|
246
|
+
actionResult && !actionResult.ok
|
|
247
|
+
? actionResult.message
|
|
248
|
+
: $snap.result && !$snap.result.ok
|
|
249
|
+
? $snap.result.message
|
|
250
|
+
: null,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
function onFieldInput(id: string, value: unknown): void {
|
|
254
|
+
controller.setValue(id, value);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── File uploads (SDK client mode only) ────────────────────────────────────
|
|
258
|
+
async function onFilePick(field: FormCoreField, files: FileList | null): Promise<void> {
|
|
259
|
+
if (!files || files.length === 0) return;
|
|
260
|
+
const max = Math.min(
|
|
261
|
+
field.fileConfig?.maxFiles ?? files.length,
|
|
262
|
+
files.length,
|
|
263
|
+
);
|
|
264
|
+
for (let i = 0; i < max; i++) {
|
|
265
|
+
const file = files[i];
|
|
266
|
+
if (!file) continue;
|
|
267
|
+
try {
|
|
268
|
+
await controller.uploadFile(field.id, file, { filename: file.name });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
uploadError = err instanceof Error ? err.message : "Upload failed.";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let uploadError = $state<string | null>(null);
|
|
275
|
+
|
|
276
|
+
/** `image_input`: upload one image, then store its URL as the field's value —
|
|
277
|
+
* the submitted value is a plain URL string whichever source was used. */
|
|
278
|
+
async function onImagePick(
|
|
279
|
+
field: FormCoreField,
|
|
280
|
+
files: FileList | null,
|
|
281
|
+
maxSizeMb: number,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
const file = files?.[0];
|
|
284
|
+
if (!file) return;
|
|
285
|
+
if (file.size > maxSizeMb * 1024 * 1024) {
|
|
286
|
+
uploadError = `That image is over the ${maxSizeMb}MB limit.`;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const uploaded = await controller.uploadFile(field.id, file, {
|
|
291
|
+
filename: file.name,
|
|
292
|
+
});
|
|
293
|
+
const url = (uploaded as { url?: string } | undefined)?.url;
|
|
294
|
+
if (url) controller.setValue(field.id, url);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
uploadError = err instanceof Error ? err.message : "Upload failed.";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── icon_select ───────────────────────────────────────────────────────────
|
|
301
|
+
// The value is an icon NAME. Art resolves against the platform's icon route
|
|
302
|
+
// rather than bundling an icon set, and the name list is fetched lazily (it's
|
|
303
|
+
// ~1,600 entries that change only when Lucide does). A curated
|
|
304
|
+
// `iconConfig.allowedIcons` skips the fetch entirely.
|
|
305
|
+
let iconQuery = $state("");
|
|
306
|
+
let iconIndex = $state<string[]>([]);
|
|
307
|
+
let iconIndexRequested = false;
|
|
308
|
+
|
|
309
|
+
function iconPool(field: FormCoreField): string[] {
|
|
310
|
+
const curated = field.iconConfig?.allowedIcons ?? [];
|
|
311
|
+
if (curated.length > 0) return curated;
|
|
312
|
+
if (!iconIndexRequested) {
|
|
313
|
+
iconIndexRequested = true;
|
|
314
|
+
// Resolves to [] on failure rather than throwing.
|
|
315
|
+
void fetchIconNames().then((names) => {
|
|
316
|
+
iconIndex = names;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return iconIndex;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let imageMode = $state<"upload" | "url">("upload");
|
|
323
|
+
|
|
324
|
+
// ── Render helpers (ported from FormPreview, framework-free) ───────────────
|
|
325
|
+
|
|
326
|
+
/** Stable CSS hook for a field (prefers output key, then label slug, then id). */
|
|
327
|
+
function fieldCssKey(f: FormCoreField): string {
|
|
328
|
+
const explicit =
|
|
329
|
+
(typeof f.outputKey === "string" && f.outputKey.trim()) ||
|
|
330
|
+
(typeof f.key === "string" && f.key.trim());
|
|
331
|
+
if (explicit) return explicit;
|
|
332
|
+
const lbl = typeof f.label === "string" ? f.label.trim() : "";
|
|
333
|
+
return lbl ? slugifyKey(lbl) : f.id;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function slugifyKey(raw: string): string {
|
|
337
|
+
const words = raw
|
|
338
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
339
|
+
.trim()
|
|
340
|
+
.split(/\s+/)
|
|
341
|
+
.filter(Boolean);
|
|
342
|
+
if (words.length === 0) return "";
|
|
343
|
+
const key = words
|
|
344
|
+
.map((w, i) =>
|
|
345
|
+
i === 0
|
|
346
|
+
? w.toLowerCase()
|
|
347
|
+
: w.charAt(0).toUpperCase() + w.slice(1).toLowerCase(),
|
|
348
|
+
)
|
|
349
|
+
.join("");
|
|
350
|
+
return /^[0-9]/.test(key) ? `_${key}` : key;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const LABELLESS = new Set(["title", "subtitle", "separator", "richtext"]);
|
|
354
|
+
|
|
355
|
+
/** tw-animate entrance classes for a step (directional for "slide"). */
|
|
356
|
+
function entranceClass(animation: string, forward: boolean): string {
|
|
357
|
+
switch (animation) {
|
|
358
|
+
case "fade":
|
|
359
|
+
return "animate-in fade-in duration-500";
|
|
360
|
+
case "scale":
|
|
361
|
+
return "animate-in fade-in zoom-in-95 duration-500";
|
|
362
|
+
case "slide":
|
|
363
|
+
return forward
|
|
364
|
+
? "animate-in fade-in slide-in-from-right-8 duration-500"
|
|
365
|
+
: "animate-in fade-in slide-in-from-left-8 duration-500";
|
|
366
|
+
default:
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function timeOptions(stepMinutes: number): string[] {
|
|
372
|
+
const step = Math.max(stepMinutes, 1);
|
|
373
|
+
const opts: string[] = [];
|
|
374
|
+
for (let h = 0; h < 24; h++) {
|
|
375
|
+
for (let m = 0; m < 60; m += step) {
|
|
376
|
+
opts.push(`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return opts;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function fmtTime(t: string, format: "12h" | "24h"): string {
|
|
383
|
+
if (format === "24h") return t;
|
|
384
|
+
const [hStr, mStr = "00"] = t.split(":");
|
|
385
|
+
const h = Number(hStr);
|
|
386
|
+
const ampm = h < 12 ? "AM" : "PM";
|
|
387
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
388
|
+
return `${h12}:${mStr} ${ampm}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function asObj(v: unknown): Record<string, unknown> {
|
|
392
|
+
return v && typeof v === "object" ? (v as Record<string, unknown>) : {};
|
|
393
|
+
}
|
|
394
|
+
function asStr(v: unknown): string {
|
|
395
|
+
return typeof v === "string" ? v : "";
|
|
396
|
+
}
|
|
397
|
+
function asNum(v: unknown, fallback: number): number {
|
|
398
|
+
return typeof v === "number" ? v : fallback;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Children of a `condition_block` whose rule passes; else `null`. */
|
|
402
|
+
function conditionChildren(
|
|
403
|
+
f: FormCoreField,
|
|
404
|
+
values: Record<string, unknown>,
|
|
405
|
+
): FormCoreField[] | null {
|
|
406
|
+
if (!evalCondition(f.conditionRule, values)) return null;
|
|
407
|
+
return (f.children ?? []) as FormCoreField[];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** How many leading children of a concurrent group to reveal. */
|
|
411
|
+
function revealCountFor(
|
|
412
|
+
f: FormCoreField,
|
|
413
|
+
values: Record<string, unknown>,
|
|
414
|
+
): number {
|
|
415
|
+
const kids = (f.children ?? []) as FormCoreField[];
|
|
416
|
+
return f.concurrentReveal === true
|
|
417
|
+
? concurrentRevealCount(kids, values)
|
|
418
|
+
: kids.length;
|
|
419
|
+
}
|
|
420
|
+
</script>
|
|
421
|
+
|
|
422
|
+
{#snippet fieldInput(field: FormCoreField, snapshot: FormSnapshot)}
|
|
423
|
+
{@const value = snapshot.values[field.id]}
|
|
424
|
+
{@const ac = resolveFieldAutocomplete(field, autocompleteConfig)}
|
|
425
|
+
{@const acOff = ac === "off"}
|
|
426
|
+
{#if field.type === "title"}
|
|
427
|
+
<h2 class="biab-field__title">{field.label || "Untitled"}</h2>
|
|
428
|
+
{:else if field.type === "subtitle"}
|
|
429
|
+
<h3 class="biab-field__subtitle">{field.label || ""}</h3>
|
|
430
|
+
{:else if field.type === "separator"}
|
|
431
|
+
<hr class="biab-field__separator" />
|
|
432
|
+
{:else if field.type === "richtext"}
|
|
433
|
+
<!-- richtext is pre-sanitized server-side; rendered as-is. -->
|
|
434
|
+
<div class="biab-field__richtext">{@html field.content ?? ""}</div>
|
|
435
|
+
{:else if field.type === "image"}
|
|
436
|
+
{#if asStr(field.content)}
|
|
437
|
+
<img class="biab-field__image" src={asStr(field.content)} alt={field.label ?? ""} />
|
|
438
|
+
{/if}
|
|
439
|
+
{:else if field.type === "textarea"}
|
|
440
|
+
{@const taRows = (field.textareaConfig as { rows?: number } | undefined)?.rows ?? 4}
|
|
441
|
+
<textarea
|
|
442
|
+
class="biab-textarea biab-field__control"
|
|
443
|
+
rows={taRows}
|
|
444
|
+
placeholder={field.placeholder ?? ""}
|
|
445
|
+
autocomplete={ac ?? undefined}
|
|
446
|
+
value={asStr(value)}
|
|
447
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
448
|
+
></textarea>
|
|
449
|
+
{:else if field.type === "select" && shouldSearchOptions((field.options ?? []).length, field.display)}
|
|
450
|
+
<!-- ComboBox card, or a list long enough that scrolling it is the worse
|
|
451
|
+
default. `input list` + `datalist` is native typeahead: no JS, no
|
|
452
|
+
styling opinion, which is the SDK's contract. -->
|
|
453
|
+
<input
|
|
454
|
+
class="biab-input biab-combobox biab-field__control"
|
|
455
|
+
type="text"
|
|
456
|
+
list={`biab-list-${field.id}`}
|
|
457
|
+
placeholder={field.placeholder || "Search…"}
|
|
458
|
+
value={asStr(value)}
|
|
459
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
460
|
+
/>
|
|
461
|
+
<datalist id={`biab-list-${field.id}`}>
|
|
462
|
+
{#each field.options ?? [] as opt (opt)}
|
|
463
|
+
<option value={opt}></option>
|
|
464
|
+
{/each}
|
|
465
|
+
</datalist>
|
|
466
|
+
{:else if field.type === "select"}
|
|
467
|
+
<select
|
|
468
|
+
class="biab-select biab-field__control"
|
|
469
|
+
value={asStr(value)}
|
|
470
|
+
onchange={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
471
|
+
>
|
|
472
|
+
<option value="">{field.placeholder || "Select…"}</option>
|
|
473
|
+
{#each field.options ?? [] as opt (opt)}
|
|
474
|
+
<option value={opt}>{opt}</option>
|
|
475
|
+
{/each}
|
|
476
|
+
</select>
|
|
477
|
+
{:else if field.type === "icon_select"}
|
|
478
|
+
<!-- The value is an icon NAME; art resolves against the platform's icon
|
|
479
|
+
route, so no icon library ships in the SDK bundle. -->
|
|
480
|
+
{@const names = iconPool(field)}
|
|
481
|
+
<div class="biab-iconselect">
|
|
482
|
+
<input
|
|
483
|
+
class="biab-input biab-iconselect__search"
|
|
484
|
+
type="text"
|
|
485
|
+
placeholder={field.placeholder || "Search icons…"}
|
|
486
|
+
value={iconQuery}
|
|
487
|
+
oninput={(e) => (iconQuery = e.currentTarget.value)}
|
|
488
|
+
/>
|
|
489
|
+
{#if asStr(value)}
|
|
490
|
+
<span class="biab-iconselect__current">
|
|
491
|
+
<img src={iconUrl(asStr(value))} alt={asStr(value)} width="16" height="16" />
|
|
492
|
+
<button type="button" class="biab-iconselect__clear" onclick={() => onFieldInput(field.id, "")}>×</button>
|
|
493
|
+
</span>
|
|
494
|
+
{/if}
|
|
495
|
+
<div class="biab-iconselect__grid">
|
|
496
|
+
{#each filterIconNames(names, iconQuery) as name (name)}
|
|
497
|
+
<button
|
|
498
|
+
type="button"
|
|
499
|
+
class="biab-iconselect__item"
|
|
500
|
+
class:is-selected={asStr(value) === name}
|
|
501
|
+
title={name}
|
|
502
|
+
aria-label={name}
|
|
503
|
+
aria-pressed={asStr(value) === name}
|
|
504
|
+
onclick={() => onFieldInput(field.id, name)}
|
|
505
|
+
>
|
|
506
|
+
<img src={iconUrl(name)} alt="" width="16" height="16" />
|
|
507
|
+
</button>
|
|
508
|
+
{/each}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
{:else if field.type === "image_input"}
|
|
512
|
+
<!-- One image, either source; the submitted value is a plain URL string. -->
|
|
513
|
+
{@const cfg = field.imageInputConfig ?? {}}
|
|
514
|
+
{@const sources = cfg.sources ?? "both"}
|
|
515
|
+
<div class="biab-imageinput">
|
|
516
|
+
{#if sources === "both"}
|
|
517
|
+
<div class="biab-imageinput__modes">
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
class="biab-imageinput__mode"
|
|
521
|
+
class:is-active={imageMode === "upload"}
|
|
522
|
+
aria-pressed={imageMode === "upload"}
|
|
523
|
+
onclick={() => (imageMode = "upload")}>Upload</button
|
|
524
|
+
>
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
class="biab-imageinput__mode"
|
|
528
|
+
class:is-active={imageMode === "url"}
|
|
529
|
+
aria-pressed={imageMode === "url"}
|
|
530
|
+
onclick={() => (imageMode = "url")}>Paste URL</button
|
|
531
|
+
>
|
|
532
|
+
</div>
|
|
533
|
+
{/if}
|
|
534
|
+
{#if imageMode === "upload" && sources !== "url" && hasClient}
|
|
535
|
+
<input
|
|
536
|
+
class="biab-input biab-imageinput__file"
|
|
537
|
+
type="file"
|
|
538
|
+
accept={(cfg.allowedTypes ?? ["image/*"]).join(",")}
|
|
539
|
+
onchange={(e) => onImagePick(field, e.currentTarget.files, cfg.maxSize ?? 5)}
|
|
540
|
+
/>
|
|
541
|
+
{:else}
|
|
542
|
+
<input
|
|
543
|
+
class="biab-input biab-imageinput__url"
|
|
544
|
+
type="url"
|
|
545
|
+
placeholder={field.placeholder || "https://example.com/image.png"}
|
|
546
|
+
value={asStr(value)}
|
|
547
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
548
|
+
/>
|
|
549
|
+
{/if}
|
|
550
|
+
{#if cfg.showPreview !== false && asStr(value)}
|
|
551
|
+
<img class="biab-imageinput__preview" src={asStr(value)} alt="" />
|
|
552
|
+
{/if}
|
|
553
|
+
</div>
|
|
554
|
+
{:else if field.type === "radio"}
|
|
555
|
+
<div class="biab-field__radios">
|
|
556
|
+
{#each field.options ?? [] as opt (opt)}
|
|
557
|
+
<label class="biab-field__radio">
|
|
558
|
+
<input
|
|
559
|
+
type="radio"
|
|
560
|
+
name={field.id}
|
|
561
|
+
value={opt}
|
|
562
|
+
checked={asStr(value) === opt}
|
|
563
|
+
onchange={() => onFieldInput(field.id, opt)}
|
|
564
|
+
/>
|
|
565
|
+
<span>{opt}</span>
|
|
566
|
+
</label>
|
|
567
|
+
{/each}
|
|
568
|
+
</div>
|
|
569
|
+
{:else if field.type === "checkbox"}
|
|
570
|
+
{@const arr = Array.isArray(value) ? (value as string[]) : []}
|
|
571
|
+
<div class="biab-field__checks">
|
|
572
|
+
{#each field.options ?? [] as opt (opt)}
|
|
573
|
+
<label class="biab-field__check">
|
|
574
|
+
<input
|
|
575
|
+
type="checkbox"
|
|
576
|
+
checked={arr.includes(opt)}
|
|
577
|
+
onchange={(e) =>
|
|
578
|
+
onFieldInput(
|
|
579
|
+
field.id,
|
|
580
|
+
e.currentTarget.checked
|
|
581
|
+
? [...arr, opt]
|
|
582
|
+
: arr.filter((x) => x !== opt),
|
|
583
|
+
)}
|
|
584
|
+
/>
|
|
585
|
+
<span>{opt}</span>
|
|
586
|
+
</label>
|
|
587
|
+
{/each}
|
|
588
|
+
</div>
|
|
589
|
+
{:else if field.type === "boolean"}
|
|
590
|
+
<label class="biab-field__toggle">
|
|
591
|
+
<input
|
|
592
|
+
type="checkbox"
|
|
593
|
+
checked={value === true}
|
|
594
|
+
onchange={(e) => onFieldInput(field.id, e.currentTarget.checked)}
|
|
595
|
+
/>
|
|
596
|
+
<span>{field.placeholder || "Yes / No"}</span>
|
|
597
|
+
</label>
|
|
598
|
+
{:else if field.type === "date"}
|
|
599
|
+
<input
|
|
600
|
+
class="biab-input biab-field__control"
|
|
601
|
+
type="date"
|
|
602
|
+
value={asStr(value)}
|
|
603
|
+
onchange={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
604
|
+
/>
|
|
605
|
+
{:else if field.type === "time_picker"}
|
|
606
|
+
{@const mode = field.timeConfig?.mode ?? "simple"}
|
|
607
|
+
{#if mode === "simple"}
|
|
608
|
+
<select
|
|
609
|
+
class="biab-select biab-field__control"
|
|
610
|
+
value={asStr(value)}
|
|
611
|
+
onchange={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
612
|
+
>
|
|
613
|
+
<option value="">{field.placeholder || "Select time"}</option>
|
|
614
|
+
{#each timeOptions(field.timeConfig?.stepMinutes ?? 30) as o (o)}
|
|
615
|
+
<option value={o}>{fmtTime(o, field.timeConfig?.format ?? "12h")}</option>
|
|
616
|
+
{/each}
|
|
617
|
+
</select>
|
|
618
|
+
{:else if mode === "time_block"}
|
|
619
|
+
{@const v = asObj(value)}
|
|
620
|
+
<div class="biab-field__grid2">
|
|
621
|
+
<label class="biab-field__sub">
|
|
622
|
+
<span class="biab-field__sublabel">Start</span>
|
|
623
|
+
<input
|
|
624
|
+
class="biab-input"
|
|
625
|
+
type="time"
|
|
626
|
+
value={asStr(v.start)}
|
|
627
|
+
onchange={(e) =>
|
|
628
|
+
onFieldInput(field.id, { ...v, start: e.currentTarget.value })}
|
|
629
|
+
/>
|
|
630
|
+
</label>
|
|
631
|
+
<label class="biab-field__sub">
|
|
632
|
+
<span class="biab-field__sublabel">End</span>
|
|
633
|
+
<input
|
|
634
|
+
class="biab-input"
|
|
635
|
+
type="time"
|
|
636
|
+
value={asStr(v.end)}
|
|
637
|
+
onchange={(e) =>
|
|
638
|
+
onFieldInput(field.id, { ...v, end: e.currentTarget.value })}
|
|
639
|
+
/>
|
|
640
|
+
</label>
|
|
641
|
+
</div>
|
|
642
|
+
{:else}
|
|
643
|
+
{@const v = asObj(value)}
|
|
644
|
+
<div class="biab-field__grid2">
|
|
645
|
+
<label class="biab-field__sub">
|
|
646
|
+
<span class="biab-field__sublabel">Date</span>
|
|
647
|
+
<input
|
|
648
|
+
class="biab-input"
|
|
649
|
+
type="date"
|
|
650
|
+
value={asStr(v.date)}
|
|
651
|
+
onchange={(e) =>
|
|
652
|
+
onFieldInput(field.id, { ...v, date: e.currentTarget.value })}
|
|
653
|
+
/>
|
|
654
|
+
</label>
|
|
655
|
+
<label class="biab-field__sub">
|
|
656
|
+
<span class="biab-field__sublabel">Time</span>
|
|
657
|
+
<input
|
|
658
|
+
class="biab-input"
|
|
659
|
+
type="time"
|
|
660
|
+
value={asStr(v.time)}
|
|
661
|
+
onchange={(e) =>
|
|
662
|
+
onFieldInput(field.id, { ...v, time: e.currentTarget.value })}
|
|
663
|
+
/>
|
|
664
|
+
</label>
|
|
665
|
+
</div>
|
|
666
|
+
{/if}
|
|
667
|
+
{:else if field.type === "date_range"}
|
|
668
|
+
{@const v = asObj(value)}
|
|
669
|
+
<div class="biab-field__grid2">
|
|
670
|
+
<label class="biab-field__sub">
|
|
671
|
+
<span class="biab-field__sublabel">Start</span>
|
|
672
|
+
<input
|
|
673
|
+
class="biab-input"
|
|
674
|
+
type="date"
|
|
675
|
+
value={asStr(v.start)}
|
|
676
|
+
onchange={(e) =>
|
|
677
|
+
onFieldInput(field.id, { ...v, start: e.currentTarget.value })}
|
|
678
|
+
/>
|
|
679
|
+
</label>
|
|
680
|
+
<label class="biab-field__sub">
|
|
681
|
+
<span class="biab-field__sublabel">End</span>
|
|
682
|
+
<input
|
|
683
|
+
class="biab-input"
|
|
684
|
+
type="date"
|
|
685
|
+
value={asStr(v.end)}
|
|
686
|
+
onchange={(e) =>
|
|
687
|
+
onFieldInput(field.id, { ...v, end: e.currentTarget.value })}
|
|
688
|
+
/>
|
|
689
|
+
</label>
|
|
690
|
+
</div>
|
|
691
|
+
{:else if field.type === "slider"}
|
|
692
|
+
{@const min = field.sliderConfig?.min ?? 0}
|
|
693
|
+
{@const max = field.sliderConfig?.max ?? 100}
|
|
694
|
+
{@const step = field.sliderConfig?.step ?? 1}
|
|
695
|
+
{@const v = asNum(value, min)}
|
|
696
|
+
<div class="biab-field__slider">
|
|
697
|
+
<div class="biab-field__slider-meta">
|
|
698
|
+
<span>{min}</span><span>{v}</span><span>{max}</span>
|
|
699
|
+
</div>
|
|
700
|
+
<input
|
|
701
|
+
type="range"
|
|
702
|
+
{min}
|
|
703
|
+
{max}
|
|
704
|
+
{step}
|
|
705
|
+
value={v}
|
|
706
|
+
oninput={(e) => onFieldInput(field.id, Number(e.currentTarget.value))}
|
|
707
|
+
/>
|
|
708
|
+
</div>
|
|
709
|
+
{:else if field.type === "star_rating"}
|
|
710
|
+
{@const max = field.starRatingConfig?.maxStars ?? 5}
|
|
711
|
+
{@const v = asNum(value, 0)}
|
|
712
|
+
{@const stars = Array.from({ length: max }, (_unused, i) => i)}
|
|
713
|
+
<div class="biab-field__stars">
|
|
714
|
+
{#each stars as i (i)}
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
class="biab-field__star"
|
|
718
|
+
class:biab-field__star--on={i < v}
|
|
719
|
+
aria-label={`Rate ${i + 1}`}
|
|
720
|
+
onclick={() => onFieldInput(field.id, i + 1)}
|
|
721
|
+
>
|
|
722
|
+
★
|
|
723
|
+
</button>
|
|
724
|
+
{/each}
|
|
725
|
+
</div>
|
|
726
|
+
{:else if field.type === "happiness_rating"}
|
|
727
|
+
{@const v = asNum(value, -1)}
|
|
728
|
+
<div class="biab-field__happiness">
|
|
729
|
+
{#each ["😞", "😕", "😐", "🙂", "😄"] as emoji, i (emoji)}
|
|
730
|
+
<button
|
|
731
|
+
type="button"
|
|
732
|
+
class="biab-field__emoji"
|
|
733
|
+
class:biab-field__emoji--on={i === v}
|
|
734
|
+
onclick={() => onFieldInput(field.id, i)}
|
|
735
|
+
>
|
|
736
|
+
{emoji}
|
|
737
|
+
</button>
|
|
738
|
+
{/each}
|
|
739
|
+
</div>
|
|
740
|
+
{:else if field.type === "stepper"}
|
|
741
|
+
{@const v = asNum(value, 0)}
|
|
742
|
+
<div class="biab-field__stepper">
|
|
743
|
+
<button type="button" class="biab-btn biab-btn--ghost" onclick={() => onFieldInput(field.id, v - 1)}>−</button>
|
|
744
|
+
<span class="biab-field__stepper-value">{v}</span>
|
|
745
|
+
<button type="button" class="biab-btn biab-btn--ghost" onclick={() => onFieldInput(field.id, v + 1)}>+</button>
|
|
746
|
+
</div>
|
|
747
|
+
{:else if field.type === "address"}
|
|
748
|
+
{@const v = asObj(value)}
|
|
749
|
+
<div class="biab-field__address">
|
|
750
|
+
<label class="biab-field__sub">
|
|
751
|
+
<span class="biab-field__sublabel">Address Line 1</span>
|
|
752
|
+
<input class="biab-input" autocomplete={acOff ? "off" : "street-address"} value={asStr(v.line1)}
|
|
753
|
+
oninput={(e) => onFieldInput(field.id, { ...v, line1: e.currentTarget.value })} />
|
|
754
|
+
</label>
|
|
755
|
+
<label class="biab-field__sub">
|
|
756
|
+
<span class="biab-field__sublabel">Address Line 2</span>
|
|
757
|
+
<input class="biab-input" autocomplete={acOff ? "off" : "address-line2"} value={asStr(v.line2)}
|
|
758
|
+
oninput={(e) => onFieldInput(field.id, { ...v, line2: e.currentTarget.value })} />
|
|
759
|
+
</label>
|
|
760
|
+
<label class="biab-field__sub">
|
|
761
|
+
<span class="biab-field__sublabel">City</span>
|
|
762
|
+
<input class="biab-input" autocomplete={acOff ? "off" : "address-level2"} value={asStr(v.city)}
|
|
763
|
+
oninput={(e) => onFieldInput(field.id, { ...v, city: e.currentTarget.value })} />
|
|
764
|
+
</label>
|
|
765
|
+
<label class="biab-field__sub">
|
|
766
|
+
<span class="biab-field__sublabel">State</span>
|
|
767
|
+
<input class="biab-input" autocomplete={acOff ? "off" : "address-level1"} value={asStr(v.state)}
|
|
768
|
+
oninput={(e) => onFieldInput(field.id, { ...v, state: e.currentTarget.value })} />
|
|
769
|
+
</label>
|
|
770
|
+
<label class="biab-field__sub">
|
|
771
|
+
<span class="biab-field__sublabel">Zip</span>
|
|
772
|
+
<input class="biab-input" autocomplete={acOff ? "off" : "postal-code"} value={asStr(v.zip)}
|
|
773
|
+
oninput={(e) => onFieldInput(field.id, { ...v, zip: e.currentTarget.value })} />
|
|
774
|
+
</label>
|
|
775
|
+
</div>
|
|
776
|
+
{:else if field.type === "color_picker"}
|
|
777
|
+
{@const v = asStr(value)}
|
|
778
|
+
<div class="biab-field__colors">
|
|
779
|
+
{#each field.options ?? [] as color (color)}
|
|
780
|
+
<button
|
|
781
|
+
type="button"
|
|
782
|
+
class="biab-field__color"
|
|
783
|
+
class:biab-field__color--on={v === color}
|
|
784
|
+
style={`background-color:${color}`}
|
|
785
|
+
title={color}
|
|
786
|
+
aria-label={color}
|
|
787
|
+
onclick={() => onFieldInput(field.id, color)}
|
|
788
|
+
></button>
|
|
789
|
+
{/each}
|
|
790
|
+
</div>
|
|
791
|
+
{:else if field.type === "file_upload"}
|
|
792
|
+
{@const list = Array.isArray(value) ? (value as Array<{ name?: string; size?: number }>) : []}
|
|
793
|
+
{#if hasClient}
|
|
794
|
+
<div class="biab-field__file">
|
|
795
|
+
<input
|
|
796
|
+
type="file"
|
|
797
|
+
multiple={(field.fileConfig?.maxFiles ?? 1) > 1}
|
|
798
|
+
accept={field.fileConfig?.allowedTypes?.join(",") || undefined}
|
|
799
|
+
disabled={snapshot.uploading}
|
|
800
|
+
onchange={(e) => onFilePick(field, e.currentTarget.files)}
|
|
801
|
+
/>
|
|
802
|
+
{#if snapshot.uploading}<span class="biab-field__file-status">Uploading…</span>{/if}
|
|
803
|
+
{#if list.length > 0}
|
|
804
|
+
<ul class="biab-field__file-list">
|
|
805
|
+
{#each list as f, i (i)}
|
|
806
|
+
<li>{f.name ?? "file"}{f.size ? ` · ${Math.round((f.size ?? 0) / 1024)} KB` : ""}</li>
|
|
807
|
+
{/each}
|
|
808
|
+
</ul>
|
|
809
|
+
{/if}
|
|
810
|
+
{#if uploadError}<p class="biab-field__error">{uploadError}</p>{/if}
|
|
811
|
+
</div>
|
|
812
|
+
{:else}
|
|
813
|
+
<div class="biab-field__file biab-field__file--disabled">
|
|
814
|
+
File upload requires an SDK client (uploads are disabled in schema-only mode).
|
|
815
|
+
</div>
|
|
816
|
+
{/if}
|
|
817
|
+
{:else if field.type === "phone"}
|
|
818
|
+
<input
|
|
819
|
+
class="biab-input biab-field__control"
|
|
820
|
+
type="tel"
|
|
821
|
+
placeholder={field.placeholder ||
|
|
822
|
+
(field.phoneConfig?.international ? "+1 (555) 000-0000" : "(555) 000-0000")}
|
|
823
|
+
autocomplete={ac ?? undefined}
|
|
824
|
+
value={asStr(value)}
|
|
825
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
826
|
+
/>
|
|
827
|
+
{:else if field.type === "email"}
|
|
828
|
+
<input
|
|
829
|
+
class="biab-input biab-field__control"
|
|
830
|
+
type="email"
|
|
831
|
+
placeholder={field.placeholder || "you@example.com"}
|
|
832
|
+
autocomplete={ac ?? undefined}
|
|
833
|
+
value={asStr(value)}
|
|
834
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
835
|
+
/>
|
|
836
|
+
{:else if field.type === "number"}
|
|
837
|
+
<input
|
|
838
|
+
class="biab-input biab-field__control"
|
|
839
|
+
type="number"
|
|
840
|
+
placeholder={field.placeholder ?? ""}
|
|
841
|
+
value={typeof value === "number" ? value : asStr(value)}
|
|
842
|
+
oninput={(e) =>
|
|
843
|
+
onFieldInput(field.id, e.currentTarget.value === "" ? "" : Number(e.currentTarget.value))}
|
|
844
|
+
/>
|
|
845
|
+
{:else}
|
|
846
|
+
<input
|
|
847
|
+
class="biab-input biab-field__control"
|
|
848
|
+
placeholder={field.placeholder ?? ""}
|
|
849
|
+
autocomplete={ac ?? undefined}
|
|
850
|
+
value={asStr(value)}
|
|
851
|
+
oninput={(e) => onFieldInput(field.id, e.currentTarget.value)}
|
|
852
|
+
/>
|
|
853
|
+
{/if}
|
|
854
|
+
{/snippet}
|
|
855
|
+
|
|
856
|
+
{#snippet fieldRow(field: FormCoreField, snapshot: FormSnapshot)}
|
|
857
|
+
{@const errored = Boolean(snapshot.errors[field.id])}
|
|
858
|
+
<div
|
|
859
|
+
class="biab-field biab-field--{field.type}"
|
|
860
|
+
class:biab-field--required={field.required}
|
|
861
|
+
class:biab-field--error={errored}
|
|
862
|
+
data-field-id={field.id}
|
|
863
|
+
data-biab-field={fieldCssKey(field)}
|
|
864
|
+
data-biab-type={field.type}
|
|
865
|
+
>
|
|
866
|
+
{#if !LABELLESS.has(field.type)}
|
|
867
|
+
<span class="biab-label biab-field__label">
|
|
868
|
+
{field.label || ""}{#if field.required}<span class="biab-field__required"> *</span>{/if}
|
|
869
|
+
</span>
|
|
870
|
+
{/if}
|
|
871
|
+
{@render fieldInput(field, snapshot)}
|
|
872
|
+
{#if errored}
|
|
873
|
+
<p class="biab-field__error">{snapshot.errors[field.id]?.message}</p>
|
|
874
|
+
{/if}
|
|
875
|
+
{#if field.helpText}
|
|
876
|
+
<p class="biab-field__help">{field.helpText}</p>
|
|
877
|
+
{/if}
|
|
878
|
+
</div>
|
|
879
|
+
{/snippet}
|
|
880
|
+
|
|
881
|
+
<!-- The recursive tree walk — mirrors FormPreview's RenderTree. -->
|
|
882
|
+
{#snippet renderTree(nodes: FormCoreField[], snapshot: FormSnapshot, flat: boolean)}
|
|
883
|
+
<div class="biab-tree">
|
|
884
|
+
{#each nodes as f (f.id)}
|
|
885
|
+
{#if f.type === "input_group"}
|
|
886
|
+
{@const kids = (f.children ?? []) as FormCoreField[]}
|
|
887
|
+
{@const reveal = revealCountFor(f, snapshot.values)}
|
|
888
|
+
<div class="biab-group" class:biab-group--card={!flat} data-biab-group={fieldCssKey(f)}>
|
|
889
|
+
{#if f.label && f.showGroupLabel}
|
|
890
|
+
<div class="biab-group__label">{f.label}</div>
|
|
891
|
+
{/if}
|
|
892
|
+
{@render renderTree(kids.slice(0, reveal), snapshot, false)}
|
|
893
|
+
</div>
|
|
894
|
+
{:else if f.type === "condition_block"}
|
|
895
|
+
{@const passed = conditionChildren(f, snapshot.values)}
|
|
896
|
+
{#if passed}
|
|
897
|
+
{@render renderTree(passed, snapshot, flat)}
|
|
898
|
+
{/if}
|
|
899
|
+
{:else if f.type === "or_condition"}
|
|
900
|
+
{@const errored = Boolean(snapshot.errors[f.id])}
|
|
901
|
+
{@const left = f.children?.[0]}
|
|
902
|
+
{@const right = f.children?.[1]}
|
|
903
|
+
<div class="biab-or" class:biab-or--error={errored}>
|
|
904
|
+
{#if f.label}
|
|
905
|
+
<div class="biab-or__label">
|
|
906
|
+
{f.label}<span class="biab-or__hint"> Fill at least one side</span>
|
|
907
|
+
</div>
|
|
908
|
+
{/if}
|
|
909
|
+
<div class="biab-or__cols">
|
|
910
|
+
<div class="biab-or__col">
|
|
911
|
+
<div class="biab-or__col-label">{f.orConditionConfig?.leftLabel || "Option A"}</div>
|
|
912
|
+
{#if left}{@render renderTree([left], snapshot, false)}{/if}
|
|
913
|
+
</div>
|
|
914
|
+
<div class="biab-or__sep">OR</div>
|
|
915
|
+
<div class="biab-or__col">
|
|
916
|
+
<div class="biab-or__col-label">{f.orConditionConfig?.rightLabel || "Option B"}</div>
|
|
917
|
+
{#if right}{@render renderTree([right], snapshot, false)}{/if}
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
{#if errored}
|
|
921
|
+
<p class="biab-field__error">Fill in at least one side to continue.</p>
|
|
922
|
+
{/if}
|
|
923
|
+
</div>
|
|
924
|
+
{:else if f.type === "flex_row"}
|
|
925
|
+
{@const rowKids = (f.children ?? []) as FormCoreField[]}
|
|
926
|
+
<div class="biab-flex-row">
|
|
927
|
+
{#each rowKids as child (child.id)}
|
|
928
|
+
<div class="biab-flex-row__cell">
|
|
929
|
+
{@render fieldRow(child, snapshot)}
|
|
930
|
+
</div>
|
|
931
|
+
{/each}
|
|
932
|
+
</div>
|
|
933
|
+
{:else}
|
|
934
|
+
{@render fieldRow(f, snapshot)}
|
|
935
|
+
{/if}
|
|
936
|
+
{/each}
|
|
937
|
+
</div>
|
|
938
|
+
{/snippet}
|
|
939
|
+
|
|
940
|
+
<section class={rootClass}>
|
|
941
|
+
{#if $snap.orgIcon}
|
|
942
|
+
<img class="biab-form__org-icon" src={$snap.orgIcon} alt="" />
|
|
943
|
+
{/if}
|
|
944
|
+
|
|
945
|
+
{#if $snap.status === "loading"}
|
|
946
|
+
<div class="biab-loading">Loading form…</div>
|
|
947
|
+
{:else if $snap.status === "error"}
|
|
948
|
+
<div class="biab-empty biab-form__error">{$snap.loadError ?? "Couldn't load the form."}</div>
|
|
949
|
+
{:else if submitted}
|
|
950
|
+
<div class="biab-card biab-form__success" role="status">
|
|
951
|
+
<span class="biab-badge">Received</span>
|
|
952
|
+
<p>{successMessage}</p>
|
|
953
|
+
</div>
|
|
954
|
+
{:else}
|
|
955
|
+
{#if $snap.title || $snap.description}
|
|
956
|
+
<div class="biab-form__lead">
|
|
957
|
+
{#if $snap.title}<h2 class="biab-section__title">{$snap.title}</h2>{/if}
|
|
958
|
+
{#if $snap.description}<p class="biab-section__sub">{$snap.description}</p>{/if}
|
|
959
|
+
</div>
|
|
960
|
+
{/if}
|
|
961
|
+
|
|
962
|
+
<!-- Multi-step progress: "steps" indicator. -->
|
|
963
|
+
{#if $snap.settings.isConsecutive && $snap.settings.showProgress && $snap.settings.progressStyle !== "bar" && $snap.stepCount > 1}
|
|
964
|
+
<div class="biab-progress biab-progress--steps">
|
|
965
|
+
<div class="biab-progress__heading">Form Progress</div>
|
|
966
|
+
<ol class="biab-progress__steps">
|
|
967
|
+
{#each $snap.steps as s, i (i)}
|
|
968
|
+
{@const head = s[0]}
|
|
969
|
+
{@const stepLabel = head?.stepTitle || head?.label || `Step ${i + 1}`}
|
|
970
|
+
<li
|
|
971
|
+
class="biab-progress__step"
|
|
972
|
+
class:biab-progress__step--current={i === $snap.step}
|
|
973
|
+
class:biab-progress__step--done={i < $snap.step}
|
|
974
|
+
>
|
|
975
|
+
<button type="button" class="biab-progress__dot" onclick={() => controller.goToStep(i)}>
|
|
976
|
+
{i < $snap.step ? "✓" : i + 1}
|
|
977
|
+
</button>
|
|
978
|
+
<span class="biab-progress__label">{stepLabel}</span>
|
|
979
|
+
</li>
|
|
980
|
+
{/each}
|
|
981
|
+
</ol>
|
|
982
|
+
</div>
|
|
983
|
+
{/if}
|
|
984
|
+
|
|
985
|
+
<!-- Multi-step progress: thin "bar". -->
|
|
986
|
+
{#if $snap.settings.isConsecutive && $snap.settings.showProgress && $snap.settings.progressStyle === "bar" && $snap.stepCount > 1}
|
|
987
|
+
<div class="biab-progress biab-progress--bar">
|
|
988
|
+
<span class="biab-progress__count">{$snap.step + 1} / {$snap.stepCount}</span>
|
|
989
|
+
<div class="biab-progress__track">
|
|
990
|
+
<div class="biab-progress__fill" style={`width:${$snap.progressPercent}%`}></div>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
{/if}
|
|
994
|
+
|
|
995
|
+
{#if $snap.errorList.length > 0}
|
|
996
|
+
<div class="biab-form__errors" role="alert">
|
|
997
|
+
<div class="biab-form__errors-title">
|
|
998
|
+
Please complete the highlighted required field{$snap.errorList.length === 1 ? "" : "s"}.
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="biab-form__errors-list">
|
|
1001
|
+
{$snap.errorList.map((e) => e.label).join(", ")}
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
{/if}
|
|
1005
|
+
|
|
1006
|
+
<!-- The active step's subtree (single-page = all fields). -->
|
|
1007
|
+
{#if $snap.settings.animation === "scroll"}
|
|
1008
|
+
<div class="biab-tree">
|
|
1009
|
+
{#each $snap.visibleFields as vf (vf.id)}
|
|
1010
|
+
{@render renderTree([vf], $snap, $snap.settings.isConsecutive)}
|
|
1011
|
+
{/each}
|
|
1012
|
+
</div>
|
|
1013
|
+
{:else}
|
|
1014
|
+
{#key $snap.step}
|
|
1015
|
+
<div class={entranceClass($snap.settings.animation, $snap.forward)}>
|
|
1016
|
+
{@render renderTree($snap.visibleFields, $snap, $snap.settings.isConsecutive)}
|
|
1017
|
+
</div>
|
|
1018
|
+
{/key}
|
|
1019
|
+
{/if}
|
|
1020
|
+
|
|
1021
|
+
{#if submitError}
|
|
1022
|
+
<div class="biab-form__submit-error" role="alert">{submitError}</div>
|
|
1023
|
+
{/if}
|
|
1024
|
+
|
|
1025
|
+
<!-- Nav: multi-step (Back / Next / Submit) or single-page (Submit). -->
|
|
1026
|
+
{#if $snap.settings.isConsecutive && $snap.stepCount > 1}
|
|
1027
|
+
<div class="biab-nav">
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
class="biab-btn biab-btn--ghost biab-btn--back"
|
|
1031
|
+
disabled={$snap.isFirstStep}
|
|
1032
|
+
onclick={() => controller.back()}
|
|
1033
|
+
>
|
|
1034
|
+
Back
|
|
1035
|
+
</button>
|
|
1036
|
+
{#if !$snap.isLastStep}
|
|
1037
|
+
<button
|
|
1038
|
+
type="button"
|
|
1039
|
+
class="biab-btn biab-btn--next"
|
|
1040
|
+
disabled={!$snap.canAdvance}
|
|
1041
|
+
onclick={() => controller.next()}
|
|
1042
|
+
>
|
|
1043
|
+
Next
|
|
1044
|
+
</button>
|
|
1045
|
+
{:else}
|
|
1046
|
+
<button
|
|
1047
|
+
type="button"
|
|
1048
|
+
class="biab-btn biab-btn--submit"
|
|
1049
|
+
disabled={submitting}
|
|
1050
|
+
onclick={() => void handleSubmit()}
|
|
1051
|
+
>
|
|
1052
|
+
{submitting ? "Sending…" : submitLabel}
|
|
1053
|
+
</button>
|
|
1054
|
+
{/if}
|
|
1055
|
+
</div>
|
|
1056
|
+
{:else}
|
|
1057
|
+
<div class="biab-nav">
|
|
1058
|
+
<button
|
|
1059
|
+
type="button"
|
|
1060
|
+
class="biab-btn biab-btn--submit"
|
|
1061
|
+
disabled={submitting}
|
|
1062
|
+
onclick={() => void handleSubmit()}
|
|
1063
|
+
>
|
|
1064
|
+
{submitting ? "Sending…" : submitLabel}
|
|
1065
|
+
</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
{/if}
|
|
1068
|
+
{/if}
|
|
1069
|
+
</section>
|