@aiaiai-pt/design-system 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,14 @@
2
2
  // DS-internal sibling imports (this component lives in the DS now, #73 73f —
3
3
  // converged so admin preview AND portal runtime mount the SAME renderer).
4
4
  import Badge from "./Badge.svelte";
5
+ import Button from "./Button.svelte";
6
+ import Alert from "./Alert.svelte";
5
7
  import CodeBlock from "./CodeBlock.svelte";
6
8
  import Input from "./Input.svelte";
9
+ import MapPicker from "./MapPicker.svelte";
7
10
  import Select from "./Select.svelte";
8
11
  import Tag from "./Tag.svelte";
12
+ import type { Snippet } from "svelte";
9
13
  import {
10
14
  buildActionPayload,
11
15
  placementConsequenceRows as buildPlacementConsequenceRows,
@@ -39,6 +43,15 @@
39
43
  criteria?: Entity[];
40
44
  };
41
45
 
46
+ /**
47
+ * The `apply` seam (see dev_docs/adl/action-form-apply-seam.md). The renderer
48
+ * owns the form + validation + submit button + state; the CONSUMER injects how
49
+ * to actually send it. Foundry's `applyAction` model: every consumer converges
50
+ * on the one action engine; only transport/auth/captcha differ. Receives the
51
+ * normalized ActionPayload; resolves `{ ok }` (and may navigate on success).
52
+ */
53
+ type ApplyResult = { ok: boolean; status?: number; error?: string };
54
+
42
55
  interface Props {
43
56
  action: Entity | null;
44
57
  placement?: Entity | null;
@@ -46,6 +59,13 @@
46
59
  criteria?: Entity[];
47
60
  mode?: RendererMode;
48
61
  schema?: ActionSchema | null;
62
+ /** Injected apply. Required for a working submit button; absent → no button
63
+ * (so admin-preview / adapter-preview stay read-only). */
64
+ onApply?: (payload: Record<string, unknown>) => Promise<ApplyResult>;
65
+ /** Environment-specific captcha (e.g. Turnstile), rendered above the button
66
+ * in submit modes. */
67
+ captcha?: Snippet;
68
+ submitLabel?: string;
49
69
  }
50
70
 
51
71
  let {
@@ -55,10 +75,24 @@
55
75
  criteria = [],
56
76
  mode = "admin-preview",
57
77
  schema = null,
78
+ onApply = undefined,
79
+ captcha = undefined,
80
+ submitLabel = "Submit",
58
81
  }: Props = $props();
59
82
 
60
83
  let values = $state<Record<string, unknown>>({});
61
84
 
85
+ // Submit state (apply seam). Only meaningful in submit modes with an onApply.
86
+ let submitting = $state(false);
87
+ let submitError = $state<string | null>(null);
88
+ let submitted = $state(false);
89
+
90
+ // A form is a SUBMIT form (button shown) only in the submit modes AND when the
91
+ // consumer wired an apply. Preview modes (admin-preview/adapter-preview) and a
92
+ // missing onApply stay read-only — the button never renders.
93
+ const isSubmitMode = $derived(mode === "public-submit" || mode === "admin-execute");
94
+ const showSubmit = $derived(isSubmitMode && onApply !== undefined);
95
+
62
96
  const renderedAction = $derived((schema?.action as Entity | null | undefined) ?? action);
63
97
  const renderedPlacement = $derived((schema?.placement as Entity | null | undefined) ?? placement);
64
98
  const renderedParameters = $derived((schema?.parameters as Entity[] | undefined) ?? parameters);
@@ -80,6 +114,36 @@
80
114
  const sections = $derived(schemaSections ?? groupIntoSections(visibleParameters));
81
115
  const payload = $derived(buildPayload());
82
116
  const payloadJson = $derived(JSON.stringify(payload, null, 2));
117
+
118
+ // Client-side submit gate (layer 1 — see the ADL): every required, visible
119
+ // parameter must have a non-empty value. Server-side submission criteria are
120
+ // enforced by the BFF on submit and surfaced via `submitError`.
121
+ function isEmpty(value: unknown): boolean {
122
+ return value === undefined || value === null || value === "";
123
+ }
124
+ const canSubmit = $derived(
125
+ visibleParameters
126
+ .filter((parameter) => parameter.required)
127
+ .every((parameter) => !isEmpty(values[parameterKey(parameter)])),
128
+ );
129
+
130
+ async function handleSubmit(): Promise<void> {
131
+ if (!onApply || submitting || !canSubmit) return;
132
+ submitError = null;
133
+ submitting = true;
134
+ try {
135
+ const result = await onApply(buildPayload());
136
+ if (result.ok) {
137
+ submitted = true; // a consumer that navigates (portal → tracker) supersedes this
138
+ } else {
139
+ submitError = result.error ?? "We couldn't submit your form. Please try again.";
140
+ }
141
+ } catch {
142
+ submitError = "Something went wrong. Please try again.";
143
+ } finally {
144
+ submitting = false;
145
+ }
146
+ }
83
147
  const criteriaSummary = $derived(
84
148
  renderedCriteria
85
149
  .filter((criterion) => criterion.is_active !== false)
@@ -286,6 +350,23 @@
286
350
  ]}
287
351
  onchange={(value: string) => setValue(key, value === "true")}
288
352
  />
353
+ {:else if type === "geo"}
354
+ <!-- A `geo` parameter renders the DS map-picker; its value is the
355
+ [lon, lat] the BFF's GEO_PARSE binding transform turns into a Point.
356
+ The renderer stays generic — geo is just another param type, the
357
+ occurrence/location coupling lives entirely in the ontology binding. -->
358
+ <MapPicker
359
+ mode="point"
360
+ height="20rem"
361
+ label={String(parameter.label ?? key)}
362
+ center={Array.isArray(parameter.default_value)
363
+ ? (parameter.default_value as [number, number])
364
+ : undefined}
365
+ value={Array.isArray(values[key])
366
+ ? (values[key] as [number, number])
367
+ : undefined}
368
+ onchange={(coords: [number, number]) => setValue(key, coords)}
369
+ />
289
370
  {:else}
290
371
  <Input
291
372
  label={String(parameter.label ?? key)}
@@ -332,6 +413,31 @@
332
413
  </form>
333
414
  {/if}
334
415
 
416
+ <!-- Submit (apply seam) — only in submit modes with an injected onApply, so
417
+ preview modes never render a button. Captcha snippet sits above it. -->
418
+ {#if showSubmit && renderedAction && orderedParameters.length > 0}
419
+ <div class="submit-area">
420
+ {#if submitted}
421
+ <Alert variant="success">Your submission was received.</Alert>
422
+ {:else}
423
+ {#if submitError}
424
+ <Alert variant="error">{submitError}</Alert>
425
+ {/if}
426
+ {#if captcha}
427
+ <div class="submit-captcha">{@render captcha()}</div>
428
+ {/if}
429
+ <Button
430
+ variant="primary"
431
+ loading={submitting}
432
+ disabled={submitting || !canSubmit}
433
+ onclick={handleSubmit}
434
+ >
435
+ {submitLabel}
436
+ </Button>
437
+ {/if}
438
+ </div>
439
+ {/if}
440
+
335
441
  {#if mode === "admin-preview"}
336
442
  <div class="admin-preview">
337
443
  <section class="preview-block">
@@ -420,12 +526,22 @@
420
526
 
421
527
  .rendered-form,
422
528
  .admin-preview,
423
- .preview-block {
529
+ .preview-block,
530
+ .submit-area {
424
531
  display: flex;
425
532
  flex-direction: column;
426
533
  gap: var(--space-md);
427
534
  }
428
535
 
536
+ .submit-area {
537
+ align-items: flex-start;
538
+ gap: var(--space-lg);
539
+ }
540
+
541
+ .submit-captcha {
542
+ min-height: var(--space-4xl);
543
+ }
544
+
429
545
  .preview-block h4 {
430
546
  margin: 0;
431
547
  font-size: var(--type-body-size);
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from "svelte";
1
2
  import { type RendererMode } from "./action-form-renderer-payload";
2
3
  /**
3
4
  * The renderer treats every parameter/action/placement as a loose record
@@ -22,6 +23,18 @@ type ActionSchema = {
22
23
  parameters?: Entity[];
23
24
  criteria?: Entity[];
24
25
  };
26
+ /**
27
+ * The `apply` seam (see dev_docs/adl/action-form-apply-seam.md). The renderer
28
+ * owns the form + validation + submit button + state; the CONSUMER injects how
29
+ * to actually send it. Foundry's `applyAction` model: every consumer converges
30
+ * on the one action engine; only transport/auth/captcha differ. Receives the
31
+ * normalized ActionPayload; resolves `{ ok }` (and may navigate on success).
32
+ */
33
+ type ApplyResult = {
34
+ ok: boolean;
35
+ status?: number;
36
+ error?: string;
37
+ };
25
38
  interface Props {
26
39
  action: Entity | null;
27
40
  placement?: Entity | null;
@@ -29,6 +42,13 @@ interface Props {
29
42
  criteria?: Entity[];
30
43
  mode?: RendererMode;
31
44
  schema?: ActionSchema | null;
45
+ /** Injected apply. Required for a working submit button; absent → no button
46
+ * (so admin-preview / adapter-preview stay read-only). */
47
+ onApply?: (payload: Record<string, unknown>) => Promise<ApplyResult>;
48
+ /** Environment-specific captcha (e.g. Turnstile), rendered above the button
49
+ * in submit modes. */
50
+ captcha?: Snippet;
51
+ submitLabel?: string;
32
52
  }
33
53
  declare const ActionFormRenderer: import("svelte").Component<Props, {}, "">;
34
54
  type ActionFormRenderer = ReturnType<typeof ActionFormRenderer>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",