@aiaiai-pt/design-system 0.10.1 → 0.14.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>;
@@ -1,7 +1,7 @@
1
1
  export default Breadcrumb;
2
2
  type Breadcrumb = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
5
  };
6
6
  /**
7
7
  * Breadcrumb
@@ -22,17 +22,13 @@ type Breadcrumb = {
22
22
  * max_items={4}
23
23
  * />
24
24
  */
25
- declare const Breadcrumb: import("svelte").Component<
26
- {
25
+ declare const Breadcrumb: import("svelte").Component<{
27
26
  items?: any[];
28
27
  max_items?: any;
29
28
  class?: string;
30
- } & Record<string, any>,
31
- {},
32
- ""
33
- >;
29
+ } & Record<string, any>, {}, "">;
34
30
  type $$ComponentProps = {
35
- items?: any[];
36
- max_items?: any;
37
- class?: string;
31
+ items?: any[];
32
+ max_items?: any;
33
+ class?: string;
38
34
  } & Record<string, any>;
@@ -1,7 +1,7 @@
1
1
  export default Button;
2
2
  type Button = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
5
  };
6
6
  /**
7
7
  * Button
@@ -32,8 +32,7 @@ type Button = {
32
32
  * {#snippet icon()}<PhGear size={16} />{/snippet}
33
33
  * </Button>
34
34
  */
35
- declare const Button: import("svelte").Component<
36
- {
35
+ declare const Button: import("svelte").Component<{
37
36
  variant?: string;
38
37
  size?: string;
39
38
  loading?: boolean;
@@ -41,24 +40,21 @@ declare const Button: import("svelte").Component<
41
40
  iconOnly?: boolean;
42
41
  type?: string;
43
42
  href?: any;
44
- ref?: HTMLElement | undefined;
43
+ ref?: any;
45
44
  class?: string;
46
45
  icon?: any;
47
46
  children?: any;
48
- } & Record<string, any>,
49
- {},
50
- "ref"
51
- >;
47
+ } & Record<string, any>, {}, "ref">;
52
48
  type $$ComponentProps = {
53
- variant?: string;
54
- size?: string;
55
- loading?: boolean;
56
- disabled?: boolean;
57
- iconOnly?: boolean;
58
- type?: string;
59
- href?: any;
60
- ref?: HTMLElement | undefined;
61
- class?: string;
62
- icon?: any;
63
- children?: any;
49
+ variant?: string;
50
+ size?: string;
51
+ loading?: boolean;
52
+ disabled?: boolean;
53
+ iconOnly?: boolean;
54
+ type?: string;
55
+ href?: any;
56
+ ref?: any;
57
+ class?: string;
58
+ icon?: any;
59
+ children?: any;
64
60
  } & Record<string, any>;
@@ -14,6 +14,11 @@
14
14
  <Card interactive selected={isSelected} onclick={() => select(id)}>
15
15
  Clickable card
16
16
  </Card>
17
+
18
+ @example Link card (navigation — renders an <a>, like Button's href)
19
+ <Card href="/submit/potholes">
20
+ Report a pothole
21
+ </Card>
17
22
  -->
18
23
  <script>
19
24
  /**
@@ -27,17 +32,26 @@
27
32
  interactive = false,
28
33
  /** @type {boolean} */
29
34
  selected = false,
35
+ /** @type {string | undefined} — renders an <a> when set (navigation card) */
36
+ href = undefined,
30
37
  /** @type {string} */
31
38
  class: className = '',
32
39
  /** @type {import('svelte').Snippet | undefined} */
33
40
  children = undefined,
34
41
  ...rest
35
42
  } = $props();
36
-
37
- // tag variable unused — rendering uses {#if interactive} branching
38
43
  </script>
39
44
 
40
- {#if interactive}
45
+ {#if href}
46
+ <a
47
+ {href}
48
+ class="card card-{variant} card-interactive card-link {className}"
49
+ class:card-selected={selected}
50
+ {...rest}
51
+ >
52
+ {#if children}{@render children()}{/if}
53
+ </a>
54
+ {:else if interactive}
41
55
  <button
42
56
  class="card card-{variant} card-interactive {className}"
43
57
  class:card-selected={selected}
@@ -102,6 +116,12 @@
102
116
  outline-offset: var(--focus-ring-offset);
103
117
  }
104
118
 
119
+ /* Anchor reset — a link card reads as a card, not as link text. */
120
+ .card-link {
121
+ text-decoration: none;
122
+ color: inherit;
123
+ }
124
+
105
125
  .card-selected {
106
126
  border-color: var(--card-selected-border-color);
107
127
  }
@@ -19,11 +19,17 @@ type Card = {
19
19
  * <Card interactive selected={isSelected} onclick={() => select(id)}>
20
20
  * Clickable card
21
21
  * </Card>
22
+ *
23
+ * @example Link card (navigation — renders an <a>, like Button's href)
24
+ * <Card href="/submit/potholes">
25
+ * Report a pothole
26
+ * </Card>
22
27
  */
23
28
  declare const Card: import("svelte").Component<{
24
29
  variant?: string;
25
30
  interactive?: boolean;
26
31
  selected?: boolean;
32
+ href?: any;
27
33
  class?: string;
28
34
  children?: any;
29
35
  } & Record<string, any>, {}, "">;
@@ -31,6 +37,7 @@ type $$ComponentProps = {
31
37
  variant?: string;
32
38
  interactive?: boolean;
33
39
  selected?: boolean;
40
+ href?: any;
34
41
  class?: string;
35
42
  children?: any;
36
43
  } & Record<string, any>;
@@ -1,13 +1,13 @@
1
1
  export default Combobox;
2
2
  export type ComboboxItem = {
3
- value: string;
4
- label: string;
5
- group?: string;
6
- description?: string;
3
+ value: string;
4
+ label: string;
5
+ group?: string;
6
+ description?: string;
7
7
  };
8
8
  type Combobox = {
9
- $on?(type: string, callback: (e: any) => void): () => void;
10
- $set?(props: Partial<$$ComponentProps>): void;
9
+ $on?(type: string, callback: (e: any) => void): () => void;
10
+ $set?(props: Partial<$$ComponentProps>): void;
11
11
  };
12
12
  /**
13
13
  * Combobox
@@ -34,8 +34,7 @@ type Combobox = {
34
34
  * bind:value={selected}
35
35
  * />
36
36
  */
37
- declare const Combobox: import("svelte").Component<
38
- {
37
+ declare const Combobox: import("svelte").Component<{
39
38
  items?: any[];
40
39
  value?: string;
41
40
  label?: any;
@@ -45,26 +44,21 @@ declare const Combobox: import("svelte").Component<
45
44
  help?: any;
46
45
  size?: string;
47
46
  onchange?: any;
48
- /** Async search callback; when set, disables internal filtering. Parent must update `items` with results. */
49
- onsearch?: (query: string) => void;
50
- /** Show "Searching…" in dropdown while loading async results */
47
+ onsearch?: any;
51
48
  loading?: boolean;
52
49
  class?: string;
53
- } & Record<string, any>,
54
- {},
55
- "value"
56
- >;
50
+ } & Record<string, any>, {}, "value">;
57
51
  type $$ComponentProps = {
58
- items?: any[];
59
- value?: string;
60
- label?: any;
61
- placeholder?: string;
62
- disabled?: boolean;
63
- error?: any;
64
- help?: any;
65
- size?: string;
66
- onchange?: any;
67
- onsearch?: (query: string) => void;
68
- loading?: boolean;
69
- class?: string;
52
+ items?: any[];
53
+ value?: string;
54
+ label?: any;
55
+ placeholder?: string;
56
+ disabled?: boolean;
57
+ error?: any;
58
+ help?: any;
59
+ size?: string;
60
+ onchange?: any;
61
+ onsearch?: any;
62
+ loading?: boolean;
63
+ class?: string;
70
64
  } & Record<string, any>;
@@ -1,7 +1,14 @@
1
1
  export default DataTable;
2
+ export type ColumnDef = {
3
+ key: string;
4
+ label: string;
5
+ width?: string;
6
+ sortable?: boolean;
7
+ render?: (value: unknown, row: Record<string, unknown>) => string;
8
+ };
2
9
  type DataTable = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
10
+ $on?(type: string, callback: (e: any) => void): () => void;
11
+ $set?(props: Partial<$$ComponentProps>): void;
5
12
  };
6
13
  /**
7
14
  * DataTable
@@ -32,9 +39,30 @@ type DataTable = {
32
39
  *
33
40
  * @example Loading
34
41
  * <DataTable {columns} rows={[]} loading />
42
+ *
43
+ * @example Responsive (default)
44
+ * Tables card-stack below `card_breakpoint` (default 768px) so columns
45
+ * don't scroll off-screen on phones/tablets. The first column becomes the
46
+ * card title; the rest render as label:value rows. Opt out with
47
+ * `responsive={false}`, or raise the breakpoint for wide tables.
48
+ * <DataTable {columns} {rows} card_breakpoint={1024} />
49
+ *
50
+ * @example Custom cell rendering (badges, chips, components)
51
+ * Pass a `cell` snippet to override the default per-cell text rendering.
52
+ * The default behavior (using `column.render` to produce a string) is
53
+ * preserved when no `cell` snippet is provided.
54
+ *
55
+ * <DataTable {columns} {rows}>
56
+ * {#snippet cell({ row, column, value })}
57
+ * {#if column.key === 'status'}
58
+ * <Badge variant={statusVariant(value)}>{value}</Badge>
59
+ * {:else}
60
+ * {value ?? '-'}
61
+ * {/if}
62
+ * {/snippet}
63
+ * </DataTable>
35
64
  */
36
- declare const DataTable: import("svelte").Component<
37
- {
65
+ declare const DataTable: import("svelte").Component<{
38
66
  columns?: any[];
39
67
  rows?: any[];
40
68
  loading?: boolean;
@@ -42,7 +70,7 @@ declare const DataTable: import("svelte").Component<
42
70
  sort_key?: any;
43
71
  sort_direction?: string;
44
72
  selectable?: boolean;
45
- selected_rows?: Set<string>;
73
+ selected_rows?: any;
46
74
  row_key?: string;
47
75
  responsive?: boolean;
48
76
  card_breakpoint?: number;
@@ -51,27 +79,28 @@ declare const DataTable: import("svelte").Component<
51
79
  on_sort?: any;
52
80
  on_select?: any;
53
81
  on_row_click?: any;
82
+ cell?: any;
54
83
  children?: any;
55
84
  class?: string;
56
- } & Record<string, any>,
57
- {},
58
- "selected_rows"
59
- >;
85
+ } & Record<string, any>, {}, "selected_rows">;
60
86
  type $$ComponentProps = {
61
- columns?: any[];
62
- rows?: any[];
63
- loading?: boolean;
64
- sortable?: boolean;
65
- sort_key?: any;
66
- sort_direction?: string;
67
- selectable?: boolean;
68
- selected_rows?: Set<string>;
69
- row_key?: string;
70
- empty_heading?: string;
71
- empty_body?: string;
72
- on_sort?: any;
73
- on_select?: any;
74
- on_row_click?: any;
75
- children?: any;
76
- class?: string;
87
+ columns?: any[];
88
+ rows?: any[];
89
+ loading?: boolean;
90
+ sortable?: boolean;
91
+ sort_key?: any;
92
+ sort_direction?: string;
93
+ selectable?: boolean;
94
+ selected_rows?: any;
95
+ row_key?: string;
96
+ responsive?: boolean;
97
+ card_breakpoint?: number;
98
+ empty_heading?: string;
99
+ empty_body?: string;
100
+ on_sort?: any;
101
+ on_select?: any;
102
+ on_row_click?: any;
103
+ cell?: any;
104
+ children?: any;
105
+ class?: string;
77
106
  } & Record<string, any>;
@@ -6,9 +6,10 @@ type DateRangePicker = {
6
6
  /**
7
7
  * DateRangePicker
8
8
  *
9
- * Start/end date pair for filters and ranges. Two coordinated DatePickers
10
- * where start constrains end and vice versa.
11
- * Consumes --datepicker-* and --input-* tokens from components.css.
9
+ * Integrated calendar for selecting a date range. Single calendar with
10
+ * two-phase selection: click start hover preview click end.
11
+ * Range highlighted between start and end dates.
12
+ * Consumes --datepicker-* tokens from components.css.
12
13
  *
13
14
  * @example Basic
14
15
  * <DateRangePicker label="PERIOD" bind:start bind:end />
@@ -32,8 +33,9 @@ declare const DateRangePicker: import("svelte").Component<{
32
33
  displayFormat?: string;
33
34
  locale?: typeof enUS;
34
35
  onchange?: any;
36
+ id?: any;
35
37
  class?: string;
36
- } & Record<string, any>, {}, "start" | "end">;
38
+ } & Record<string, any>, {}, "end" | "start">;
37
39
  type $$ComponentProps = {
38
40
  start?: any;
39
41
  end?: any;
@@ -50,6 +52,7 @@ type $$ComponentProps = {
50
52
  displayFormat?: string;
51
53
  locale?: typeof enUS;
52
54
  onchange?: any;
55
+ id?: any;
53
56
  class?: string;
54
57
  } & Record<string, any>;
55
58
  import { enUS } from 'date-fns/locale';
@@ -6,7 +6,7 @@ type DateTimePicker = {
6
6
  /**
7
7
  * DateTimePicker
8
8
  *
9
- * Date + time selection. Composes DatePicker with a time input.
9
+ * Date + time selection. Composes DatePicker with styled hour/minute selects.
10
10
  * Values displayed in Berkeley Mono (data font).
11
11
  * Consumes --datepicker-* and --input-* tokens from components.css.
12
12
  *
@@ -1,23 +1,20 @@
1
1
  export default FilterPanel;
2
2
  export type FilterOption = {
3
- value: string;
4
- label: string;
3
+ value: string;
4
+ label: string;
5
5
  };
6
6
  export type FilterFieldDef = {
7
- key: string;
8
- label: string;
9
- type: "select" | "boolean" | "search";
10
- options?: FilterOption[];
11
- /** Async search callback for 'search' type filters (FK relationships) */
12
- onsearch?: (query: string) => void;
13
- /** Items for 'search' type, managed by parent */
14
- searchItems?: FilterOption[];
15
- /** Loading state for 'search' type */
16
- searchLoading?: boolean;
7
+ key: string;
8
+ label: string;
9
+ type: "select" | "boolean" | "search";
10
+ options?: FilterOption[];
11
+ onsearch?: (query: string) => void;
12
+ searchItems?: FilterOption[];
13
+ searchLoading?: boolean;
17
14
  };
18
15
  type FilterPanel = {
19
- $on?(type: string, callback: (e: any) => void): () => void;
20
- $set?(props: Partial<$$ComponentProps>): void;
16
+ $on?(type: string, callback: (e: any) => void): () => void;
17
+ $set?(props: Partial<$$ComponentProps>): void;
21
18
  };
22
19
  /**
23
20
  * FilterPanel
@@ -26,6 +23,8 @@ type FilterPanel = {
26
23
  * Each filter renders as the appropriate widget based on its type:
27
24
  * select → Select dropdown, boolean → Toggle pair, search → Combobox.
28
25
  *
26
+ * Consumes --filter-chip-*, --filter-bar-clear-*, and --input-* tokens from components.css.
27
+ *
29
28
  * @example
30
29
  * <FilterPanel
31
30
  * filters={[
@@ -39,21 +38,17 @@ type FilterPanel = {
39
38
  * onchange={handleFilterChange}
40
39
  * />
41
40
  */
42
- declare const FilterPanel: import("svelte").Component<
43
- {
44
- filters?: FilterFieldDef[];
41
+ declare const FilterPanel: import("svelte").Component<{
42
+ filters?: any[];
45
43
  value?: Record<string, any>;
46
- onchange?: (value: Record<string, any>) => void;
47
- onclear?: () => void;
44
+ onchange?: any;
45
+ onclear?: any;
48
46
  class?: string;
49
- } & Record<string, any>,
50
- {},
51
- "value"
52
- >;
47
+ } & Record<string, any>, {}, "value">;
53
48
  type $$ComponentProps = {
54
- filters?: FilterFieldDef[];
55
- value?: Record<string, any>;
56
- onchange?: (value: Record<string, any>) => void;
57
- onclear?: () => void;
58
- class?: string;
49
+ filters?: any[];
50
+ value?: Record<string, any>;
51
+ onchange?: any;
52
+ onclear?: any;
53
+ class?: string;
59
54
  } & Record<string, any>;
@@ -6,25 +6,23 @@ type GeoSearch = {
6
6
  /**
7
7
  * GeoSearch
8
8
  *
9
- * Geocoding search input powered by Nominatim (OpenStreetMap).
10
- * Wraps Combobox with built-in address/place search and debounced API calls.
11
- * Emits lon/lat coordinates when a result is selected.
9
+ * Bidirectional geocoding input powered by Nominatim (OpenStreetMap).
10
+ * Forward: type address get coordinates.
11
+ * Reverse: set `coords` prop shows resolved address.
12
12
  * Consumes --combobox-* tokens from components.css.
13
13
  *
14
- * @example Basic
14
+ * @example Forward geocoding
15
15
  * <GeoSearch onlocation={(lon, lat) => console.log(lon, lat)} />
16
16
  *
17
- * @example With label and bounding box bias
17
+ * @example Bidirectional (MapPicker sets coords on click → address shows)
18
18
  * <GeoSearch
19
- * label="Search location"
20
- * placeholder="City, address, or place..."
21
- * viewbox={[-9.5, 38.5, -8.8, 39.0]}
22
- * onlocation={(lon, lat) => { mapCenter = [lon, lat]; mapZoom = 15; }}
19
+ * bind:coords={pickedCoords}
20
+ * onlocation={(lon, lat) => { mapCenter = [lon, lat]; }}
23
21
  * />
24
22
  *
25
- * @example Custom provider URL
23
+ * @example With viewbox bias
26
24
  * <GeoSearch
27
- * providerUrl="https://my-nominatim.example.com/search"
25
+ * viewbox={[-9.5, 38.5, -8.8, 39.0]}
28
26
  * onlocation={(lon, lat) => console.log(lon, lat)}
29
27
  * />
30
28
  */
@@ -38,8 +36,9 @@ declare const GeoSearch: import("svelte").Component<{
38
36
  providerUrl?: string;
39
37
  viewbox?: any;
40
38
  onlocation?: any;
39
+ coords?: any;
41
40
  class?: string;
42
- } & Record<string, any>, {}, "">;
41
+ } & Record<string, any>, {}, "coords">;
43
42
  type $$ComponentProps = {
44
43
  label?: any;
45
44
  placeholder?: string;
@@ -50,5 +49,6 @@ type $$ComponentProps = {
50
49
  providerUrl?: string;
51
50
  viewbox?: any;
52
51
  onlocation?: any;
52
+ coords?: any;
53
53
  class?: string;
54
54
  } & Record<string, any>;
@@ -13,6 +13,9 @@
13
13
 
14
14
  @example Custom heading level (when the hero is not the page's H1)
15
15
  <Hero title="Latest consultations" headingLevel={2} />
16
+
17
+ @example Background image (a theme-tinted scrim keeps text contrast)
18
+ <Hero title="Report a problem" image="/images/city.jpg" />
16
19
  -->
17
20
  <script>
18
21
  let {
@@ -22,6 +25,9 @@
22
25
  subtitle = "",
23
26
  /** @type {1 | 2 | 3} Heading level for `title` — keep one h1 per page. */
24
27
  headingLevel = 1,
28
+ /** @type {string | undefined} Background image URL — rendered cover/center
29
+ * under a `--hero-scrim` overlay so the text tokens keep contrast. */
30
+ image = undefined,
25
31
  /** @type {string} */
26
32
  class: className = "",
27
33
  /** @type {import('svelte').Snippet | undefined} Title override (rich content). */
@@ -30,9 +36,21 @@
30
36
  actions = undefined,
31
37
  ...rest
32
38
  } = $props();
39
+
40
+ // CSS-string escape for the url() value — the image URL is operator data.
41
+ const bgStyle = $derived(
42
+ image
43
+ ? `background-image: url('${image.replace(/\\/g, "%5C").replace(/'/g, "%27")}')`
44
+ : undefined,
45
+ );
33
46
  </script>
34
47
 
35
- <section class="hero {className}" {...rest}>
48
+ <section
49
+ class="hero {className}"
50
+ class:hero-has-image={!!image}
51
+ style={bgStyle}
52
+ {...rest}
53
+ >
36
54
  <div class="hero-inner">
37
55
  {#if children}
38
56
  {@render children()}
@@ -53,6 +71,24 @@
53
71
  background: var(--color-surface);
54
72
  }
55
73
 
74
+ .hero-has-image {
75
+ position: relative;
76
+ background-size: cover;
77
+ background-position: center;
78
+ }
79
+
80
+ /* Theme-tinted scrim between the image and the text (contrast guard). */
81
+ .hero-has-image::before {
82
+ content: "";
83
+ position: absolute;
84
+ inset: 0;
85
+ background: var(--hero-scrim);
86
+ }
87
+
88
+ .hero-has-image .hero-inner {
89
+ position: relative;
90
+ }
91
+
56
92
  .hero-inner {
57
93
  width: 100%;
58
94
  max-width: var(--content-width-wide);
@@ -18,11 +18,15 @@ type Hero = {
18
18
  *
19
19
  * @example Custom heading level (when the hero is not the page's H1)
20
20
  * <Hero title="Latest consultations" headingLevel={2} />
21
+ *
22
+ * @example Background image (a theme-tinted scrim keeps text contrast)
23
+ * <Hero title="Report a problem" image="/images/city.jpg" />
21
24
  */
22
25
  declare const Hero: import("svelte").Component<{
23
26
  title?: string;
24
27
  subtitle?: string;
25
28
  headingLevel?: number;
29
+ image?: any;
26
30
  class?: string;
27
31
  children?: any;
28
32
  actions?: any;
@@ -31,6 +35,7 @@ type $$ComponentProps = {
31
35
  title?: string;
32
36
  subtitle?: string;
33
37
  headingLevel?: number;
38
+ image?: any;
34
39
  class?: string;
35
40
  children?: any;
36
41
  actions?: any;
@@ -9,11 +9,15 @@ type MapPicker = {
9
9
  * Interactive map for selecting a point or drawing a polygon.
10
10
  * OpenLayers with configurable tiles and Draw interaction.
11
11
  * Draw sketch styled with DS tokens (not OL blue default).
12
+ * Built-in GeoSearch (Nominatim) for address lookup + pan.
12
13
  * Consumes --map-* tokens from components.css.
13
14
  *
14
- * @example Point selection
15
+ * @example Point selection with search
15
16
  * <MapPicker mode="point" onchange={(coords) => console.log(coords)} />
16
17
  *
18
+ * @example Without search
19
+ * <MapPicker mode="point" search={false} onchange={(coords) => console.log(coords)} />
20
+ *
17
21
  * @example Polygon drawing
18
22
  * <MapPicker mode="polygon" onchange={(coords) => console.log(coords)} />
19
23
  */
@@ -26,6 +30,9 @@ declare const MapPicker: import("svelte").Component<{
26
30
  help?: any;
27
31
  error?: any;
28
32
  disabled?: boolean;
33
+ search?: boolean;
34
+ searchProviderUrl?: any;
35
+ searchViewbox?: any;
29
36
  tileSource?: Record<string, any>;
30
37
  onchange?: any;
31
38
  height?: string;
@@ -41,6 +48,9 @@ type $$ComponentProps = {
41
48
  help?: any;
42
49
  error?: any;
43
50
  disabled?: boolean;
51
+ search?: boolean;
52
+ searchProviderUrl?: any;
53
+ searchViewbox?: any;
44
54
  tileSource?: Record<string, any>;
45
55
  onchange?: any;
46
56
  height?: string;
@@ -1,13 +1,14 @@
1
1
  export default Pagination;
2
2
  type Pagination = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
5
  };
6
6
  /**
7
7
  * Pagination
8
8
  *
9
9
  * Cursor-based and offset-based pagination with page size selector.
10
- * Consumes --pagination-* tokens (falls back to semantic tokens).
10
+ * Self-contained nav controls with correct icon placement (chevron
11
+ * leading for PREV, trailing for NEXT).
11
12
  *
12
13
  * @example Cursor mode (default)
13
14
  * <Pagination
@@ -30,8 +31,7 @@ type Pagination = {
30
31
  * on_page_size_change={(s) => setSize(s)}
31
32
  * />
32
33
  */
33
- declare const Pagination: import("svelte").Component<
34
- {
34
+ declare const Pagination: import("svelte").Component<{
35
35
  mode?: string;
36
36
  has_next?: boolean;
37
37
  has_prev?: boolean;
@@ -42,25 +42,22 @@ declare const Pagination: import("svelte").Component<
42
42
  total_items?: any;
43
43
  on_page_change?: any;
44
44
  page_size?: number;
45
- page_sizes?: number[];
45
+ page_sizes?: any;
46
46
  on_page_size_change?: any;
47
47
  class?: string;
48
- } & Record<string, any>,
49
- {},
50
- ""
51
- >;
48
+ } & Record<string, any>, {}, "">;
52
49
  type $$ComponentProps = {
53
- mode?: string;
54
- has_next?: boolean;
55
- has_prev?: boolean;
56
- on_next?: any;
57
- on_prev?: any;
58
- current_page?: number;
59
- total_pages?: any;
60
- total_items?: any;
61
- on_page_change?: any;
62
- page_size?: number;
63
- page_sizes?: number[];
64
- on_page_size_change?: any;
65
- class?: string;
50
+ mode?: string;
51
+ has_next?: boolean;
52
+ has_prev?: boolean;
53
+ on_next?: any;
54
+ on_prev?: any;
55
+ current_page?: number;
56
+ total_pages?: any;
57
+ total_items?: any;
58
+ on_page_change?: any;
59
+ page_size?: number;
60
+ page_sizes?: any;
61
+ on_page_size_change?: any;
62
+ class?: string;
66
63
  } & Record<string, any>;
@@ -16,7 +16,7 @@ type SiteHeader = {
16
16
  * @example
17
17
  * <SiteHeader>
18
18
  * {#snippet brand()}<a href="/"><Logo /> Valongo</a>{/snippet}
19
- * {#snippet nav()}<a href="/report">Report</a><a href="/info/privacy">Privacy</a>{/snippet}
19
+ * {#snippet nav()}<ServiceNavigation items={navItems} />{/snippet}
20
20
  * {#snippet actions()}<LocaleSwitch /> <Button>Sign in</Button>{/snippet}
21
21
  * </SiteHeader>
22
22
  */
@@ -82,6 +82,7 @@ declare const ValueSourcePicker: import("svelte").Component<{
82
82
  value?: any;
83
83
  onchange?: any;
84
84
  allowed?: any[];
85
+ defaultMode?: any;
85
86
  context?: Record<string, any>;
86
87
  expectedType?: any;
87
88
  label?: any;
@@ -95,6 +96,7 @@ type $$ComponentProps = {
95
96
  value?: any;
96
97
  onchange?: any;
97
98
  allowed?: any[];
99
+ defaultMode?: any;
98
100
  context?: Record<string, any>;
99
101
  expectedType?: any;
100
102
  label?: any;
@@ -17,6 +17,17 @@ export { default as Progress } from "./Progress.svelte";
17
17
  export { default as List } from "./List.svelte";
18
18
  export { default as ListItem } from "./ListItem.svelte";
19
19
  export { default as PageContainer } from "./PageContainer.svelte";
20
+ export { default as AppFrame } from "./AppFrame.svelte";
21
+ export { default as SiteHeader } from "./SiteHeader.svelte";
22
+ export { default as SiteFooter } from "./SiteFooter.svelte";
23
+ export { default as SkipLink } from "./SkipLink.svelte";
24
+ export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
25
+ export { default as Link } from "./Link.svelte";
26
+ export { default as Hero } from "./Hero.svelte";
27
+ export { default as ContentBlock } from "./ContentBlock.svelte";
28
+ export { default as StatusTimeline } from "./StatusTimeline.svelte";
29
+ export { default as WidgetGrid } from "./WidgetGrid.svelte";
30
+ export { default as VotingWidget } from "./VotingWidget.svelte";
20
31
  export { default as Card } from "./Card.svelte";
21
32
  export { default as Panel } from "./Panel.svelte";
22
33
  export { default as Modal } from "./Modal.svelte";
@@ -25,8 +36,12 @@ export { default as Menu } from "./Menu.svelte";
25
36
  export { default as MenuItem } from "./MenuItem.svelte";
26
37
  export { default as MenuSeparator } from "./MenuSeparator.svelte";
27
38
  export { default as Combobox } from "./Combobox.svelte";
39
+ export { default as DatePicker } from "./DatePicker.svelte";
40
+ export { default as DateTimePicker } from "./DateTimePicker.svelte";
41
+ export { default as DateRangePicker } from "./DateRangePicker.svelte";
28
42
  export { default as SearchInput } from "./SearchInput.svelte";
29
43
  export { default as FilterBar } from "./FilterBar.svelte";
44
+ export { default as FilterPanel } from "./FilterPanel.svelte";
30
45
  export { default as CommandPalette } from "./CommandPalette.svelte";
31
46
  export { default as Tabs } from "./Tabs.svelte";
32
47
  export { default as TabList } from "./TabList.svelte";
@@ -34,26 +49,27 @@ export { default as Tab } from "./Tab.svelte";
34
49
  export { default as TabPanel } from "./TabPanel.svelte";
35
50
  export { default as Alert } from "./Alert.svelte";
36
51
  export { default as Toast } from "./Toast.svelte";
52
+ export { default as ToastManager } from "./ToastManager.svelte";
37
53
  export { default as EmptyState } from "./EmptyState.svelte";
54
+ export { default as NotificationBell } from "./NotificationBell.svelte";
38
55
  export { default as Sidebar } from "./Sidebar.svelte";
39
56
  export { default as SidebarItem } from "./SidebarItem.svelte";
40
57
  export { default as SidebarSection } from "./SidebarSection.svelte";
41
58
  export { default as BottomNav } from "./BottomNav.svelte";
42
59
  export { default as BottomNavItem } from "./BottomNavItem.svelte";
43
60
  export { default as Stepper } from "./Stepper.svelte";
61
+ export { default as ValueSourcePicker } from "./ValueSourcePicker.svelte";
44
62
  export { default as CodeBlock } from "./CodeBlock.svelte";
45
63
  export { default as CodeEditor } from "./CodeEditor.svelte";
46
64
  export { default as CollapsibleSection } from "./CollapsibleSection.svelte";
47
65
  export { default as OptionGrid } from "./OptionGrid.svelte";
48
66
  export { default as ConditionTable } from "./ConditionTable.svelte";
49
67
  export { default as LogViewer } from "./LogViewer.svelte";
68
+ export { default as Tree } from "./Tree.svelte";
69
+ export { default as TreeNode } from "./TreeNode.svelte";
50
70
  export { default as DataTable } from "./DataTable.svelte";
51
71
  export { default as Pagination } from "./Pagination.svelte";
52
72
  export { default as Breadcrumb } from "./Breadcrumb.svelte";
53
- export { default as DatePicker } from "./DatePicker.svelte";
54
- export { default as DateTimePicker } from "./DateTimePicker.svelte";
55
- export { default as DateRangePicker } from "./DateRangePicker.svelte";
56
- export { default as FilterPanel } from "./FilterPanel.svelte";
57
73
  export { default as StatCard } from "./StatCard.svelte";
58
74
  export { default as StatGrid } from "./StatGrid.svelte";
59
75
  export { default as MapDisplay } from "./MapDisplay.svelte";
@@ -61,4 +77,7 @@ export { default as MapPicker } from "./MapPicker.svelte";
61
77
  export { default as MapCluster } from "./MapCluster.svelte";
62
78
  export { default as MapHeatmap } from "./MapHeatmap.svelte";
63
79
  export { default as MapPopup } from "./MapPopup.svelte";
80
+ export { default as GeoSearch } from "./GeoSearch.svelte";
64
81
  export { default as Calendar } from "./Calendar.svelte";
82
+ export { default as ActionFormRenderer } from "./ActionFormRenderer.svelte";
83
+ export { serializeValueSource, parseValueSource } from "./ValueSourcePicker.helpers.js";
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.14.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -111,6 +111,15 @@
111
111
  --checkbox-bg-checked: var(--color-accent);
112
112
  --checkbox-check-color: var(--color-text-on-accent);
113
113
 
114
+ /* ═══════════════════════════════════════════════
115
+ HERO
116
+ ═══════════════════════════════════════════════ */
117
+
118
+ /* Scrim laid over a hero background image so the text tokens keep their
119
+ contrast on any photo. Surface-tinted (not black/white) so it follows
120
+ the theme — raise the mix % for busier imagery. */
121
+ --hero-scrim: color-mix(in srgb, var(--color-surface) 78%, transparent);
122
+
114
123
  /* ═══════════════════════════════════════════════
115
124
  CARD
116
125
  ═══════════════════════════════════════════════ */