@aiaiai-pt/design-system 0.44.1 → 0.46.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.
@@ -22,6 +22,10 @@ type ActionSchema = {
22
22
  sections?: Array<{
23
23
  key?: string;
24
24
  label?: string;
25
+ order?: number | null;
26
+ columns?: number | null;
27
+ /** Read defensively (hosts carry concrete precondition types). */
28
+ visible_when?: unknown;
25
29
  parameters?: Entity[];
26
30
  }>;
27
31
  parameters?: Entity[];
@@ -65,6 +69,17 @@ interface Props {
65
69
  * transport/auth/tenancy (the uploadFile seam pattern — the DS stays
66
70
  * presentation-only). Omitted → object_reference params render disabled. */
67
71
  searchEntities?: (typeCode: string, query: string, filter?: Record<string, string>) => Promise<SelectOption[]>;
72
+ /** Edit-time label hydration for relationship params (#34): receives the
73
+ * target entity type code and the currently-linked ids, returns
74
+ * `{value, label}` items so the picker renders labels, never raw ids.
75
+ * Same philosophy as searchEntities — the CONSUMER owns transport/auth;
76
+ * omitted → the picker falls back to showing the ids. Best-effort: a
77
+ * failed hydration never blocks the form. */
78
+ hydrateEntities?: (typeCode: string, ids: string[]) => Promise<SelectOption[]>;
79
+ /** #41 — per-field server-error binding: `{param_key: message}` rendered
80
+ * as each field's inline error (the V2a adapter re-binds proxy 4xx field
81
+ * errors here). Display-only — the HOST owns when errors set and clear. */
82
+ fieldErrors?: Record<string, string>;
68
83
  /** Environment-specific captcha (e.g. Turnstile), rendered above the button
69
84
  * in submit modes. */
70
85
  captcha?: Snippet;
@@ -35,7 +35,7 @@ declare const DateRangePicker: import("svelte").Component<{
35
35
  onchange?: any;
36
36
  id?: any;
37
37
  class?: string;
38
- } & Record<string, any>, {}, "start" | "end">;
38
+ } & Record<string, any>, {}, "end" | "start">;
39
39
  type $$ComponentProps = {
40
40
  start?: any;
41
41
  end?: any;
@@ -0,0 +1,152 @@
1
+ <!--
2
+ @component JsonEditor
3
+
4
+ JSON field over the DS CodeEditor (#38, atelier#669 V1) with an explicit
5
+ invalid-state contract: invalid text NEVER silently drops or half-parses —
6
+ the field surfaces the parse error, emits NOTHING, and reports invalidity
7
+ through `oninvalid` so the host form can block submit. Empty and `{}` are
8
+ distinct (empty → null).
9
+
10
+ @example
11
+ <JsonEditor
12
+ label="METADATA"
13
+ value={{ kind: 'sensor' }}
14
+ onchange={(v) => save(v)}
15
+ oninvalid={(message) => (blocked = message !== null)}
16
+ />
17
+ -->
18
+ <script module>
19
+ let _jsonEditorUid = 0;
20
+ </script>
21
+
22
+ <script>
23
+ import CodeEditor from './CodeEditor.svelte';
24
+ import { evaluateJsonDraft, formatJsonValue } from './json-editor-contract';
25
+
26
+ let {
27
+ /** @type {unknown} — the current JSON value (object/array/scalar/null) */
28
+ value = null,
29
+ /** @type {string | undefined} */
30
+ label = undefined,
31
+ /** @type {string | undefined} */
32
+ help = undefined,
33
+ /** @type {string | undefined} — EXTERNAL (server) error; parse errors
34
+ * render through the same chrome automatically */
35
+ error = undefined,
36
+ /** @type {boolean} */
37
+ disabled = false,
38
+ /** @type {boolean} */
39
+ readonly = false,
40
+ /** @type {((value: unknown) => void) | undefined} — fires ONLY on a
41
+ * valid parse (or empty → null); never fires for invalid text */
42
+ onchange = undefined,
43
+ /** @type {((message: string | null) => void) | undefined} — parse-state
44
+ * transitions: a message while the draft is invalid, null when it
45
+ * becomes valid again. The host blocks submit while non-null. */
46
+ oninvalid = undefined,
47
+ /** @type {string} */
48
+ class: className = '',
49
+ ...rest
50
+ } = $props();
51
+
52
+ const editorId = `json-editor-${_jsonEditorUid++}`;
53
+ const hintId = `${editorId}-hint`;
54
+
55
+ let text = $state(formatJsonValue(value));
56
+ let parseError = $state(/** @type {string | null} */ (null));
57
+
58
+ // Watch the CodeEditor-bound text: evaluate on every draft change. The
59
+ // pipeline itself is the pure json-editor-contract module (CodeMirror is
60
+ // not edit-drivable under jsdom, so the contract carries the test weight).
61
+ let lastEvaluated = text;
62
+ $effect(() => {
63
+ if (text === lastEvaluated) return;
64
+ lastEvaluated = text;
65
+ const result = evaluateJsonDraft(text);
66
+ if (result.ok) {
67
+ parseError = null;
68
+ oninvalid?.(null);
69
+ onchange?.(result.value);
70
+ } else {
71
+ parseError = result.error;
72
+ oninvalid?.(result.error);
73
+ }
74
+ });
75
+
76
+ const shownError = $derived(error ?? parseError ?? undefined);
77
+ </script>
78
+
79
+ <div class="json-editor {className}" {...rest}>
80
+ {#if label}
81
+ <span class="json-editor-label" id={`${editorId}-label`}>{label}</span>
82
+ {/if}
83
+ <div
84
+ class="json-editor-surface"
85
+ class:json-editor-error={!!shownError}
86
+ class:json-editor-disabled={disabled}
87
+ aria-labelledby={label ? `${editorId}-label` : undefined}
88
+ aria-describedby={shownError || help ? hintId : undefined}
89
+ >
90
+ <CodeEditor
91
+ bind:value={text}
92
+ language="json"
93
+ readonly={readonly || disabled}
94
+ lineNumbers={false}
95
+ minLines={4}
96
+ maxLines={16}
97
+ />
98
+ </div>
99
+ {#if shownError}
100
+ <span id={hintId} class="json-editor-error-text" role="alert">{shownError}</span>
101
+ {:else if help}
102
+ <span id={hintId} class="json-editor-help">{help}</span>
103
+ {/if}
104
+ </div>
105
+
106
+ <style>
107
+ .json-editor {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--input-label-gap);
111
+ width: 100%;
112
+ }
113
+
114
+ .json-editor-label {
115
+ font-family: var(--input-label-font);
116
+ font-size: var(--input-label-size);
117
+ letter-spacing: var(--input-label-tracking);
118
+ color: var(--input-label-color);
119
+ }
120
+
121
+ .json-editor-surface {
122
+ border: var(--input-border);
123
+ border-radius: var(--input-radius);
124
+ background: var(--input-bg);
125
+ padding: var(--space-xs);
126
+ }
127
+
128
+ .json-editor-surface:focus-within {
129
+ border: var(--input-border-focus);
130
+ }
131
+
132
+ .json-editor-error {
133
+ border-color: var(--input-error-border-color);
134
+ }
135
+
136
+ .json-editor-disabled {
137
+ opacity: 0.5;
138
+ pointer-events: none;
139
+ }
140
+
141
+ .json-editor-help {
142
+ font-family: var(--input-help-font);
143
+ font-size: var(--input-help-size);
144
+ color: var(--input-help-color);
145
+ }
146
+
147
+ .json-editor-error-text {
148
+ font-family: var(--input-help-font);
149
+ font-size: var(--input-help-size);
150
+ color: var(--input-error-text);
151
+ }
152
+ </style>
@@ -0,0 +1,44 @@
1
+ export default JsonEditor;
2
+ type JsonEditor = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * JsonEditor
8
+ *
9
+ * JSON field over the DS CodeEditor (#38, atelier#669 V1) with an explicit
10
+ * invalid-state contract: invalid text NEVER silently drops or half-parses —
11
+ * the field surfaces the parse error, emits NOTHING, and reports invalidity
12
+ * through `oninvalid` so the host form can block submit. Empty and `{}` are
13
+ * distinct (empty → null).
14
+ *
15
+ * @example
16
+ * <JsonEditor
17
+ * label="METADATA"
18
+ * value={{ kind: 'sensor' }}
19
+ * onchange={(v) => save(v)}
20
+ * oninvalid={(message) => (blocked = message !== null)}
21
+ * />
22
+ */
23
+ declare const JsonEditor: import("svelte").Component<{
24
+ value?: any;
25
+ label?: any;
26
+ help?: any;
27
+ error?: any;
28
+ disabled?: boolean;
29
+ readonly?: boolean;
30
+ onchange?: any;
31
+ oninvalid?: any;
32
+ class?: string;
33
+ } & Record<string, any>, {}, "">;
34
+ type $$ComponentProps = {
35
+ value?: any;
36
+ label?: any;
37
+ help?: any;
38
+ error?: any;
39
+ disabled?: boolean;
40
+ readonly?: boolean;
41
+ onchange?: any;
42
+ oninvalid?: any;
43
+ class?: string;
44
+ } & Record<string, any>;
@@ -5,9 +5,15 @@
5
5
  * Each section is a labelled <fieldset>, parameters laid out as a
6
6
  * single-column flex column. This is the safe fallback for unknown
7
7
  * layout keys — see `resolve.ts`.
8
+ *
9
+ * #634 S3 — a section declaring `columns: 2` lays its cells on a
10
+ * two-column grid; `span: full` params (and wide types — geo/file)
11
+ * break out to the full row (the EntityFormSurface two-col precedent).
12
+ * Default (columns 1 / absent) keeps the single-column stack
13
+ * byte-identical.
8
14
  */
9
15
  import Card from "./Card.svelte";
10
- import type { LayoutSection } from "./action-form-renderer-layouts";
16
+ import { fieldSpansFull, type LayoutSection } from "./action-form-renderer-layouts";
11
17
  import type { Snippet } from "svelte";
12
18
 
13
19
  let {
@@ -23,9 +29,16 @@
23
29
  {#each sections as section (section.name)}
24
30
  <Card variant="flat" class="section" role="group" aria-label={section.name}>
25
31
  <h4>{section.name}</h4>
26
- <div class="rows">
32
+ <div
33
+ class="rows"
34
+ class:two-col={(section.columns ?? 1) === 2}
35
+ data-columns={section.columns ?? 1}
36
+ >
27
37
  {#each section.items as parameter}
28
- <div class="field-row">
38
+ <div
39
+ class="field-row"
40
+ class:span-full={fieldSpansFull(parameter, section.columns)}
41
+ >
29
42
  {@render field(parameter)}
30
43
  </div>
31
44
  {/each}
@@ -53,6 +66,26 @@
53
66
  gap: var(--space-md);
54
67
  }
55
68
 
69
+ /* #634 S3 — two-column density: half-width cells flow the grid,
70
+ span-full cells break out to the whole row. Collapses back to one
71
+ column on narrow viewports (the EntityFormSurface breakpoint). */
72
+ .rows.two-col {
73
+ display: grid;
74
+ grid-template-columns: repeat(2, minmax(0, 1fr));
75
+ column-gap: var(--space-md);
76
+ row-gap: var(--space-md);
77
+ }
78
+
79
+ .rows.two-col .field-row.span-full {
80
+ grid-column: 1 / -1;
81
+ }
82
+
83
+ @media (max-width: 48rem) {
84
+ .rows.two-col {
85
+ grid-template-columns: 1fr;
86
+ }
87
+ }
88
+
56
89
  .field-row {
57
90
  display: flex;
58
91
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- import type { LayoutSection } from "./action-form-renderer-layouts";
1
+ import { type LayoutSection } from "./action-form-renderer-layouts";
2
2
  import type { Snippet } from "svelte";
3
3
  type $$ComponentProps = {
4
4
  sections: LayoutSection[];
@@ -0,0 +1,219 @@
1
+ <!--
2
+ @component MoneyInput
3
+
4
+ Number-semantics input with a currency affordance (#35, atelier#669 V1).
5
+ The WIRE value is always a raw number (or null when empty — empty and 0
6
+ are distinct); locale-aware currency formatting appears while the field is
7
+ NOT being edited; focus switches to the raw editable number.
8
+ Consumes --input-* tokens (mirrors Input's field chrome).
9
+
10
+ @example
11
+ <MoneyInput label="BUDGET" value={1234.5} onchange={(n) => save(n)} />
12
+
13
+ @example Non-default currency
14
+ <MoneyInput label="FEE" currency="USD" locale="en-US" />
15
+ -->
16
+ <script module>
17
+ let _moneyUid = 0;
18
+ </script>
19
+
20
+ <script>
21
+ let {
22
+ /** @type {number | null} — the raw amount; null = empty */
23
+ value = null,
24
+ /** @type {string} — ISO 4217 code for the display affordance */
25
+ currency = 'EUR',
26
+ /** @type {string | undefined} — BCP 47; defaults to the runtime locale */
27
+ locale = undefined,
28
+ /** @type {string | undefined} */
29
+ label = undefined,
30
+ /** @type {string | undefined} */
31
+ placeholder = undefined,
32
+ /** @type {string | undefined} */
33
+ help = undefined,
34
+ /** @type {string | undefined} */
35
+ error = undefined,
36
+ /** @type {'sm' | 'md' | 'lg'} */
37
+ size = 'md',
38
+ /** @type {boolean} */
39
+ disabled = false,
40
+ /** @type {boolean} */
41
+ readonly = false,
42
+ /** @type {string | undefined} */
43
+ id = undefined,
44
+ /** @type {string | undefined} */
45
+ name = undefined,
46
+ /** @type {((value: number | null) => void) | undefined} — fires with the
47
+ * RAW number (never a formatted string); null when cleared/unparseable */
48
+ onchange = undefined,
49
+ /** @type {string} */
50
+ class: className = '',
51
+ ...rest
52
+ } = $props();
53
+
54
+ const fallbackId = `money-input-${_moneyUid++}`;
55
+ const inputId = $derived(id ?? fallbackId);
56
+ const hintId = $derived(`${inputId}-hint`);
57
+ const hasHint = $derived(!!error || !!help);
58
+
59
+ let focused = $state(false);
60
+ let draft = $state('');
61
+
62
+ const formatter = $derived(
63
+ new Intl.NumberFormat(locale, { style: 'currency', currency }),
64
+ );
65
+
66
+ const display = $derived(
67
+ focused
68
+ ? draft
69
+ : value === null || value === undefined
70
+ ? ''
71
+ : formatter.format(Number(value)),
72
+ );
73
+
74
+ /**
75
+ * Parse the operator's raw text onto a number: strips grouping spaces,
76
+ * accepts a comma decimal. Empty/unparseable → null (never NaN, never a
77
+ * silent 0 — empty and 0 stay distinct).
78
+ * @param {string} text
79
+ * @returns {number | null}
80
+ */
81
+ function parseAmount(text) {
82
+ const trimmed = text.trim().replace(/\s+/g, '');
83
+ if (!trimmed) return null;
84
+ const normalized =
85
+ trimmed.includes(',') && !trimmed.includes('.')
86
+ ? trimmed.replace(',', '.')
87
+ : trimmed;
88
+ const parsed = Number(normalized);
89
+ return Number.isFinite(parsed) ? parsed : null;
90
+ }
91
+
92
+ function handleFocus() {
93
+ if (readonly || disabled) return;
94
+ draft = value === null || value === undefined ? '' : String(value);
95
+ focused = true;
96
+ }
97
+
98
+ /** @param {Event} event */
99
+ function handleInput(event) {
100
+ draft = /** @type {HTMLInputElement} */ (event.currentTarget).value;
101
+ const parsed = parseAmount(draft);
102
+ value = parsed;
103
+ onchange?.(parsed);
104
+ }
105
+
106
+ function handleBlur() {
107
+ focused = false;
108
+ }
109
+ </script>
110
+
111
+ <div class="money-input input-group {className}">
112
+ {#if label}
113
+ <label class="input-label" for={inputId}>{label}</label>
114
+ {/if}
115
+
116
+ <input
117
+ id={inputId}
118
+ {name}
119
+ type="text"
120
+ inputmode="decimal"
121
+ class="input input-{size}"
122
+ class:input-error={!!error}
123
+ class:input-readonly={readonly}
124
+ aria-invalid={error ? true : undefined}
125
+ aria-describedby={hasHint ? hintId : undefined}
126
+ {placeholder}
127
+ {disabled}
128
+ {readonly}
129
+ value={display}
130
+ onfocus={handleFocus}
131
+ oninput={handleInput}
132
+ onblur={handleBlur}
133
+ autocomplete="off"
134
+ {...rest}
135
+ />
136
+
137
+ {#if error}
138
+ <span id={hintId} class="input-error-text" role="alert">{error}</span>
139
+ {:else if help}
140
+ <span id={hintId} class="input-help">{help}</span>
141
+ {/if}
142
+ </div>
143
+
144
+ <style>
145
+ .money-input {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: var(--input-label-gap);
149
+ width: 100%;
150
+ }
151
+
152
+ .input-label {
153
+ font-family: var(--input-label-font);
154
+ font-size: var(--input-label-size);
155
+ letter-spacing: var(--input-label-tracking);
156
+ color: var(--input-label-color);
157
+ }
158
+
159
+ .input {
160
+ font-family: var(--input-font);
161
+ font-size: var(--input-font-size);
162
+ border: var(--input-border);
163
+ border-radius: var(--input-radius);
164
+ background: var(--input-bg);
165
+ color: var(--input-text);
166
+ transition: border var(--input-transition);
167
+ width: 100%;
168
+ }
169
+
170
+ .input-sm {
171
+ height: var(--input-sm-height);
172
+ padding: 0 var(--input-sm-padding-x);
173
+ }
174
+
175
+ .input-md {
176
+ height: var(--input-md-height);
177
+ padding: 0 var(--input-md-padding-x);
178
+ }
179
+
180
+ .input-lg {
181
+ height: var(--input-lg-height);
182
+ padding: 0 var(--input-lg-padding-x);
183
+ }
184
+
185
+ .input::placeholder {
186
+ color: var(--input-placeholder);
187
+ }
188
+
189
+ .input:focus {
190
+ outline: none;
191
+ border: var(--input-border-focus);
192
+ }
193
+
194
+ .input:disabled {
195
+ opacity: 0.5;
196
+ cursor: not-allowed;
197
+ }
198
+
199
+ .input-readonly {
200
+ background: var(--color-surface-secondary);
201
+ cursor: default;
202
+ }
203
+
204
+ .input-error {
205
+ border-color: var(--input-error-border-color);
206
+ }
207
+
208
+ .input-help {
209
+ font-family: var(--input-help-font);
210
+ font-size: var(--input-help-size);
211
+ color: var(--input-help-color);
212
+ }
213
+
214
+ .input-error-text {
215
+ font-family: var(--input-help-font);
216
+ font-size: var(--input-help-size);
217
+ color: var(--input-error-text);
218
+ }
219
+ </style>
@@ -0,0 +1,52 @@
1
+ export default MoneyInput;
2
+ type MoneyInput = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * MoneyInput
8
+ *
9
+ * Number-semantics input with a currency affordance (#35, atelier#669 V1).
10
+ * The WIRE value is always a raw number (or null when empty — empty and 0
11
+ * are distinct); locale-aware currency formatting appears while the field is
12
+ * NOT being edited; focus switches to the raw editable number.
13
+ * Consumes --input-* tokens (mirrors Input's field chrome).
14
+ *
15
+ * @example
16
+ * <MoneyInput label="BUDGET" value={1234.5} onchange={(n) => save(n)} />
17
+ *
18
+ * @example Non-default currency
19
+ * <MoneyInput label="FEE" currency="USD" locale="en-US" />
20
+ */
21
+ declare const MoneyInput: import("svelte").Component<{
22
+ value?: any;
23
+ currency?: string;
24
+ locale?: any;
25
+ label?: any;
26
+ placeholder?: any;
27
+ help?: any;
28
+ error?: any;
29
+ size?: string;
30
+ disabled?: boolean;
31
+ readonly?: boolean;
32
+ id?: any;
33
+ name?: any;
34
+ onchange?: any;
35
+ class?: string;
36
+ } & Record<string, any>, {}, "">;
37
+ type $$ComponentProps = {
38
+ value?: any;
39
+ currency?: string;
40
+ locale?: any;
41
+ label?: any;
42
+ placeholder?: any;
43
+ help?: any;
44
+ error?: any;
45
+ size?: string;
46
+ disabled?: boolean;
47
+ readonly?: boolean;
48
+ id?: any;
49
+ name?: any;
50
+ onchange?: any;
51
+ class?: string;
52
+ } & Record<string, any>;