@aiaiai-pt/design-system 0.45.0 → 0.46.1

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.
@@ -52,17 +52,20 @@ export interface LayoutSection {
52
52
  visibleWhen?: Record<string, unknown> | null;
53
53
  }
54
54
 
55
- /** Parameter types that can never sit half-width in a two-column section —
56
- * they carry their own wide UI (map canvas, upload list). Mirrors the CRUD
57
- * form-surface compiler's full-width clamp (form-surface.ts). */
58
- const FULL_WIDTH_PARAM_TYPES = new Set(["geo", "file", "json"]);
55
+ import {
56
+ FULL_WIDTH_WIDGET_KINDS,
57
+ widgetKind,
58
+ } from "./action-form-renderer-widgets";
59
59
 
60
60
  /**
61
61
  * #634 S3 — whether a parameter's cell spans the full row inside a
62
62
  * `columns: 2` section. An explicit `span: "full"` on the parameter wins;
63
- * wide types clamp to full regardless; everything else flows half-width
64
- * (so a bare `columns: 2` declaration visibly two-columns its fields).
65
- * In a single-column section every cell is trivially full.
63
+ * wide WIDGET KINDS (map canvas, upload list, textarea, json editor —
64
+ * resolved widget-first per #36, so both the field summary's `widget` and
65
+ * a bare legacy `type` clamp) go full regardless; everything else flows
66
+ * half-width (so a bare `columns: 2` declaration visibly two-columns its
67
+ * fields). In a single-column section every cell is trivially full.
68
+ * Mirrors the CRUD form-surface compilers' FULL_WIDTH_WIDGETS clamp.
66
69
  */
67
70
  export function fieldSpansFull(
68
71
  parameter: Record<string, unknown>,
@@ -70,7 +73,7 @@ export function fieldSpansFull(
70
73
  ): boolean {
71
74
  if ((columns ?? 1) < 2) return true;
72
75
  if (parameter.span === "full") return true;
73
- return FULL_WIDTH_PARAM_TYPES.has(String(parameter.type ?? ""));
76
+ return FULL_WIDTH_WIDGET_KINDS.has(widgetKind(parameter));
74
77
  }
75
78
 
76
79
  import type { Snippet } from "svelte";
@@ -15,6 +15,18 @@
15
15
  * Svelte component owns the reactive plumbing; this module owns the shape.
16
16
  */
17
17
  export type RendererMode = "admin-preview" | "admin-execute" | "public-submit" | "adapter-preview";
18
+ /** #41 (atelier#669 V1, operator decision D3) — readonly across the two param
19
+ * shapes: the form-surface.v1 summary says `visibility: "readonly"` (a
20
+ * STRING); the action lane says `visibility: {editable: false}` (an object,
21
+ * mirrored onto `editable` by the schema builders). */
22
+ export declare function isReadonlyParam(parameter: Record<string, unknown>): boolean;
23
+ /** #41 / D3 — whether a param's value belongs in `form.raw_values`:
24
+ * an explicit `payload: "include" | "exclude"` always wins; the
25
+ * form-surface.v1 lane (string visibility) defaults readonly OUT of the
26
+ * wire; the LEGACY action lane (object visibility, no payload key) keeps
27
+ * its #252 ride-the-payload behavior — flipping it would silently drop
28
+ * citizen identity prefills from deployed intake forms. */
29
+ export declare function isPayloadIncluded(parameter: Record<string, unknown>): boolean;
18
30
  /**
19
31
  * Minimal action/placement references the payload builder reads. Callers may
20
32
  * pass a full ontology Entity; this narrower shape is what we actually rely
@@ -14,7 +14,38 @@
14
14
  * source_schema, raw_values, mode), returns the canonical payload. The
15
15
  * Svelte component owns the reactive plumbing; this module owns the shape.
16
16
  */
17
- export type RendererMode = "admin-preview" | "admin-execute" | "public-submit" | "adapter-preview";
17
+ export type RendererMode =
18
+ "admin-preview" | "admin-execute" | "public-submit" | "adapter-preview";
19
+
20
+ /** #41 (atelier#669 V1, operator decision D3) — readonly across the two param
21
+ * shapes: the form-surface.v1 summary says `visibility: "readonly"` (a
22
+ * STRING); the action lane says `visibility: {editable: false}` (an object,
23
+ * mirrored onto `editable` by the schema builders). */
24
+ export function isReadonlyParam(parameter: Record<string, unknown>): boolean {
25
+ if (parameter.editable === false) return true;
26
+ const visibility = parameter.visibility;
27
+ if (typeof visibility === "string") return visibility === "readonly";
28
+ if (
29
+ visibility &&
30
+ typeof visibility === "object" &&
31
+ !Array.isArray(visibility)
32
+ ) {
33
+ return (visibility as Record<string, unknown>).editable === false;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ /** #41 / D3 — whether a param's value belongs in `form.raw_values`:
39
+ * an explicit `payload: "include" | "exclude"` always wins; the
40
+ * form-surface.v1 lane (string visibility) defaults readonly OUT of the
41
+ * wire; the LEGACY action lane (object visibility, no payload key) keeps
42
+ * its #252 ride-the-payload behavior — flipping it would silently drop
43
+ * citizen identity prefills from deployed intake forms. */
44
+ export function isPayloadIncluded(parameter: Record<string, unknown>): boolean {
45
+ if (parameter.payload === "include") return true;
46
+ if (parameter.payload === "exclude") return false;
47
+ return parameter.visibility !== "readonly";
48
+ }
18
49
 
19
50
  /**
20
51
  * Minimal action/placement references the payload builder reads. Callers may
@@ -106,7 +137,9 @@ function defaultSurfaceFor(mode: RendererMode): string {
106
137
  return mode === "public-submit" ? "public_submit" : "admin_preview";
107
138
  }
108
139
 
109
- function pickScope(targetConfig: Record<string, unknown>): Record<string, unknown> {
140
+ function pickScope(
141
+ targetConfig: Record<string, unknown>,
142
+ ): Record<string, unknown> {
110
143
  const scope: Record<string, unknown> = {};
111
144
  for (const [key, value] of Object.entries(targetConfig)) {
112
145
  if (RESERVED_TARGET_KEYS.has(key)) continue;
@@ -116,7 +149,17 @@ function pickScope(targetConfig: Record<string, unknown>): Record<string, unknow
116
149
  }
117
150
 
118
151
  export function buildActionPayload(args: BuildArgs): ActionPayload {
119
- const { action, placement, targetConfig, sourceSchema, rawValues, schemaVersion, mode, attachmentKeys, attachmentsByParam } = args;
152
+ const {
153
+ action,
154
+ placement,
155
+ targetConfig,
156
+ sourceSchema,
157
+ rawValues,
158
+ schemaVersion,
159
+ mode,
160
+ attachmentKeys,
161
+ attachmentsByParam,
162
+ } = args;
120
163
 
121
164
  const sourceFromSchema = nullableString(sourceSchema.source);
122
165
  const targetModel =
@@ -0,0 +1,20 @@
1
+ export type RelationshipMeta = {
2
+ relCode?: string;
3
+ targetTypeCode: string;
4
+ cardinality?: string;
5
+ };
6
+ type Entity = Record<string, unknown>;
7
+ /** The relationship meta, from the field summary's `relationship` or the
8
+ * derived-action param's `ui_schema.relationship`. Null for non-rel params. */
9
+ export declare function relationshipMeta(parameter: Entity): RelationshipMeta | null;
10
+ /** True for both rel lanes: the form-surface `relationship` summary and the
11
+ * action-lane `object_reference` param. */
12
+ export declare function isRelationshipParam(parameter: Entity): boolean;
13
+ /** Multi-value (chips) when the param says `multiple: true` (derived-action
14
+ * lane) or the relationship meta says many_to_many (form-surface lane). */
15
+ export declare function isMultiRelationship(parameter: Entity): boolean;
16
+ /** The entity type code the picker searches/hydrates against. */
17
+ export declare function relationshipTypeCode(parameter: Entity): string;
18
+ /** Normalize a rel value-bag entry onto the ID-list wire shape. */
19
+ export declare function normalizeRelIds(value: unknown): string[];
20
+ export {};
@@ -0,0 +1,77 @@
1
+ // #34 (atelier#669 V1, operator decision D6) — cardinality-aware relationship
2
+ // helpers for ActionFormRenderer. Pure functions over the two param shapes the
3
+ // renderer meets:
4
+ // form-surface.v1 field summary — `type: "relationship"` with a top-level
5
+ // `relationship: {relCode, targetTypeCode, cardinality}`;
6
+ // derived-action param — `type: "object_reference"` with `object_type`,
7
+ // `multiple: true` for many_to_many, and the same relationship meta under
8
+ // `ui_schema.relationship` (see atelier derived_crud._member_to_param).
9
+
10
+ export type RelationshipMeta = {
11
+ relCode?: string;
12
+ targetTypeCode: string;
13
+ cardinality?: string;
14
+ };
15
+
16
+ type Entity = Record<string, unknown>;
17
+
18
+ function asRecord(value: unknown): Record<string, unknown> | null {
19
+ return value && typeof value === "object" && !Array.isArray(value)
20
+ ? (value as Record<string, unknown>)
21
+ : null;
22
+ }
23
+
24
+ /** The relationship meta, from the field summary's `relationship` or the
25
+ * derived-action param's `ui_schema.relationship`. Null for non-rel params. */
26
+ export function relationshipMeta(parameter: Entity): RelationshipMeta | null {
27
+ const carrier =
28
+ asRecord(parameter.relationship) ??
29
+ asRecord(asRecord(parameter.ui_schema)?.relationship);
30
+ if (!carrier) return null;
31
+ const targetTypeCode = String(
32
+ carrier.targetTypeCode ?? carrier.target_type_code ?? "",
33
+ );
34
+ if (!targetTypeCode) return null;
35
+ const relCode = carrier.relCode ?? carrier.rel_code;
36
+ const cardinality = carrier.cardinality;
37
+ return {
38
+ relCode: relCode ? String(relCode) : undefined,
39
+ targetTypeCode,
40
+ cardinality: cardinality ? String(cardinality) : undefined,
41
+ };
42
+ }
43
+
44
+ /** True for both rel lanes: the form-surface `relationship` summary and the
45
+ * action-lane `object_reference` param. */
46
+ export function isRelationshipParam(parameter: Entity): boolean {
47
+ const type = String(parameter.type ?? "");
48
+ return (
49
+ type === "relationship" ||
50
+ type === "object_reference" ||
51
+ relationshipMeta(parameter) !== null
52
+ );
53
+ }
54
+
55
+ /** Multi-value (chips) when the param says `multiple: true` (derived-action
56
+ * lane) or the relationship meta says many_to_many (form-surface lane). */
57
+ export function isMultiRelationship(parameter: Entity): boolean {
58
+ if (parameter.multiple === true) return true;
59
+ return relationshipMeta(parameter)?.cardinality === "many_to_many";
60
+ }
61
+
62
+ /** The entity type code the picker searches/hydrates against. */
63
+ export function relationshipTypeCode(parameter: Entity): string {
64
+ return (
65
+ relationshipMeta(parameter)?.targetTypeCode ??
66
+ String(parameter.object_type ?? "")
67
+ );
68
+ }
69
+
70
+ /** Normalize a rel value-bag entry onto the ID-list wire shape. */
71
+ export function normalizeRelIds(value: unknown): string[] {
72
+ if (Array.isArray(value)) {
73
+ return value.map((entry) => String(entry ?? "")).filter(Boolean);
74
+ }
75
+ if (typeof value === "string" && value) return [value];
76
+ return [];
77
+ }
@@ -0,0 +1,39 @@
1
+ type Entity = Record<string, unknown>;
2
+ /** The render intent for a param: `widget` → `ui_schema.widget` → `type`. */
3
+ export declare function widgetKind(parameter: Entity): string;
4
+ /** Widget kinds that can never sit half-width in a two-column section —
5
+ * they carry their own wide UI. Mirrors the CRUD form-surface compilers'
6
+ * FULL_WIDTH_WIDGETS clamp (form-surface.ts / derived_crud.py). */
7
+ export declare const FULL_WIDTH_WIDGET_KINDS: Set<string>;
8
+ /** #37 — parse the platform's date-only wire shape (YYYY-MM-DD) onto a LOCAL
9
+ * calendar date. Never routes through Date.parse/toISOString: a local
10
+ * midnight serialized via UTC shifts a day across DST/negative-offset
11
+ * timezones — the classic off-by-one. */
12
+ export declare function dateOnlyToDate(value: unknown): Date | null;
13
+ /** #37 — serialize a picked Date back onto YYYY-MM-DD from its LOCAL
14
+ * calendar parts (no time component, no timezone shift). */
15
+ export declare function dateToDateOnly(date: Date): string;
16
+ export type GeoJsonPoint = {
17
+ type: "Point";
18
+ coordinates: [number, number];
19
+ };
20
+ /** #39 — hydrate the contract's GeoJSON value onto the MapPicker's
21
+ * [lon, lat] prop edge. Empty → no pin, no error. A non-Point (or
22
+ * malformed Point) fails LOUD: null coords + a named error the field
23
+ * renders — never a silently-empty map over real data. */
24
+ export declare function geoJsonPointToLonLat(value: unknown): {
25
+ coords: [number, number] | null;
26
+ error: string | null;
27
+ };
28
+ /** #39 — serialize a placed pin back onto the GeoJSON wire shape. */
29
+ export declare function lonLatToGeoJsonPoint(coords: [number, number]): GeoJsonPoint;
30
+ export type StoredFile = {
31
+ name: string;
32
+ url?: string;
33
+ };
34
+ /** #40 — normalize a file param's stored-current value (the edit form's
35
+ * prefill) onto {name, url?}. Accepts a bare name, a URL string (name =
36
+ * last path segment), or a {name?, url?} descriptor. Anything else → null
37
+ * (no stored file → the legacy append-mode upload field). */
38
+ export declare function storedFileDescriptor(value: unknown): StoredFile | null;
39
+ export {};
@@ -0,0 +1,127 @@
1
+ // #36/#37 (atelier#669 V1) — widget-first dispatch vocabulary + date-only
2
+ // serialization for ActionFormRenderer. Pure functions.
3
+ //
4
+ // The form-surface.v1 field summary carries render intent in `widget`
5
+ // (derived from the field type + legal refinements, both compilers);
6
+ // derived-action params carry the same under `ui_schema.widget`. The
7
+ // renderer dispatches on this kind and falls back to the param `type`, so
8
+ // legacy action-lane params (no widget key) render byte-identically.
9
+
10
+ type Entity = Record<string, unknown>;
11
+
12
+ /** The render intent for a param: `widget` → `ui_schema.widget` → `type`. */
13
+ export function widgetKind(parameter: Entity): string {
14
+ const direct = parameter.widget;
15
+ if (typeof direct === "string" && direct) return direct;
16
+ const uiSchema = parameter.ui_schema;
17
+ if (uiSchema && typeof uiSchema === "object" && !Array.isArray(uiSchema)) {
18
+ const widget = (uiSchema as Entity).widget;
19
+ if (typeof widget === "string" && widget) return widget;
20
+ }
21
+ return String(parameter.type ?? "string");
22
+ }
23
+
24
+ /** Widget kinds that can never sit half-width in a two-column section —
25
+ * they carry their own wide UI. Mirrors the CRUD form-surface compilers'
26
+ * FULL_WIDTH_WIDGETS clamp (form-surface.ts / derived_crud.py). */
27
+ export const FULL_WIDTH_WIDGET_KINDS = new Set([
28
+ "geo",
29
+ "file",
30
+ "json",
31
+ "textarea",
32
+ "geometry",
33
+ ]);
34
+
35
+ /** #37 — parse the platform's date-only wire shape (YYYY-MM-DD) onto a LOCAL
36
+ * calendar date. Never routes through Date.parse/toISOString: a local
37
+ * midnight serialized via UTC shifts a day across DST/negative-offset
38
+ * timezones — the classic off-by-one. */
39
+ export function dateOnlyToDate(value: unknown): Date | null {
40
+ if (typeof value !== "string") return null;
41
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
42
+ if (!match) return null;
43
+ const [, year, month, day] = match;
44
+ const date = new Date(Number(year), Number(month) - 1, Number(day));
45
+ return Number.isNaN(date.getTime()) ? null : date;
46
+ }
47
+
48
+ /** #37 — serialize a picked Date back onto YYYY-MM-DD from its LOCAL
49
+ * calendar parts (no time component, no timezone shift). */
50
+ export function dateToDateOnly(date: Date): string {
51
+ const year = String(date.getFullYear()).padStart(4, "0");
52
+ const month = String(date.getMonth() + 1).padStart(2, "0");
53
+ const day = String(date.getDate()).padStart(2, "0");
54
+ return `${year}-${month}-${day}`;
55
+ }
56
+
57
+ export type GeoJsonPoint = { type: "Point"; coordinates: [number, number] };
58
+
59
+ /** #39 — hydrate the contract's GeoJSON value onto the MapPicker's
60
+ * [lon, lat] prop edge. Empty → no pin, no error. A non-Point (or
61
+ * malformed Point) fails LOUD: null coords + a named error the field
62
+ * renders — never a silently-empty map over real data. */
63
+ export function geoJsonPointToLonLat(value: unknown): {
64
+ coords: [number, number] | null;
65
+ error: string | null;
66
+ } {
67
+ if (value === null || value === undefined || value === "") {
68
+ return { coords: null, error: null };
69
+ }
70
+ if (value && typeof value === "object" && !Array.isArray(value)) {
71
+ const row = value as Record<string, unknown>;
72
+ if (row.type === "Point" && Array.isArray(row.coordinates)) {
73
+ const [lon, lat] = row.coordinates as unknown[];
74
+ if (typeof lon === "number" && typeof lat === "number") {
75
+ return { coords: [lon, lat], error: null };
76
+ }
77
+ return {
78
+ coords: null,
79
+ error: "Unsupported geometry: malformed Point coordinates",
80
+ };
81
+ }
82
+ return {
83
+ coords: null,
84
+ error: `Unsupported geometry: expected a GeoJSON Point, got ${String(row.type ?? "unknown")}`,
85
+ };
86
+ }
87
+ return {
88
+ coords: null,
89
+ error: "Unsupported geometry: expected a GeoJSON Point",
90
+ };
91
+ }
92
+
93
+ /** #39 — serialize a placed pin back onto the GeoJSON wire shape. */
94
+ export function lonLatToGeoJsonPoint(coords: [number, number]): GeoJsonPoint {
95
+ return { type: "Point", coordinates: [coords[0], coords[1]] };
96
+ }
97
+
98
+ export type StoredFile = { name: string; url?: string };
99
+
100
+ /** #40 — normalize a file param's stored-current value (the edit form's
101
+ * prefill) onto {name, url?}. Accepts a bare name, a URL string (name =
102
+ * last path segment), or a {name?, url?} descriptor. Anything else → null
103
+ * (no stored file → the legacy append-mode upload field). */
104
+ export function storedFileDescriptor(value: unknown): StoredFile | null {
105
+ if (typeof value === "string") {
106
+ const trimmed = value.trim();
107
+ if (!trimmed) return null;
108
+ if (/^https?:\/\//i.test(trimmed)) {
109
+ const segments = trimmed.split("?")[0].split("/").filter(Boolean);
110
+ return { name: segments[segments.length - 1] || trimmed, url: trimmed };
111
+ }
112
+ return { name: trimmed };
113
+ }
114
+ if (value && typeof value === "object" && !Array.isArray(value)) {
115
+ const row = value as Record<string, unknown>;
116
+ const url = typeof row.url === "string" && row.url ? row.url : undefined;
117
+ const name =
118
+ typeof row.name === "string" && row.name
119
+ ? row.name
120
+ : url
121
+ ? (url.split("?")[0].split("/").filter(Boolean).pop() ?? url)
122
+ : null;
123
+ if (!name) return null;
124
+ return url ? { name, url } : { name };
125
+ }
126
+ return null;
127
+ }
@@ -12,6 +12,9 @@ export { default as Checkbox } from "./Checkbox.svelte";
12
12
  export { default as Label } from "./Label.svelte";
13
13
  export { default as FileUpload } from "./FileUpload.svelte";
14
14
  export { default as FileUploadItem } from "./FileUploadItem.svelte";
15
+ export { default as MultiSelectCombobox } from "./MultiSelectCombobox.svelte";
16
+ export { default as MoneyInput } from "./MoneyInput.svelte";
17
+ export { default as JsonEditor } from "./JsonEditor.svelte";
15
18
  export { default as Separator } from "./Separator.svelte";
16
19
  export { default as Progress } from "./Progress.svelte";
17
20
  export { default as List } from "./List.svelte";
@@ -25,6 +25,9 @@ export { default as Checkbox } from "./Checkbox.svelte";
25
25
  export { default as Label } from "./Label.svelte";
26
26
  export { default as FileUpload } from "./FileUpload.svelte";
27
27
  export { default as FileUploadItem } from "./FileUploadItem.svelte";
28
+ export { default as MultiSelectCombobox } from "./MultiSelectCombobox.svelte";
29
+ export { default as MoneyInput } from "./MoneyInput.svelte";
30
+ export { default as JsonEditor } from "./JsonEditor.svelte";
28
31
 
29
32
  // Layout
30
33
  export { default as Separator } from "./Separator.svelte";
@@ -0,0 +1,11 @@
1
+ export type JsonDraftResult = {
2
+ ok: true;
3
+ value: unknown;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ };
8
+ /** Pretty-print a value into the editor. Null/undefined → empty editor. */
9
+ export declare function formatJsonValue(value: unknown): string;
10
+ /** Evaluate the operator's raw text. */
11
+ export declare function evaluateJsonDraft(text: string): JsonDraftResult;
@@ -0,0 +1,33 @@
1
+ // #38 (atelier#669 V1) — the JsonEditor's parse/emit contract, pure.
2
+ // Invalid text NEVER silently drops or half-parses: evaluation either
3
+ // yields `{ok: true, value}` (emit) or `{ok: false, error}` (surface the
4
+ // parse error, block submit via the renderer's error seam, emit NOTHING).
5
+ // Empty and `{}` are distinct: empty text is `{ok: true, value: null}`.
6
+
7
+ export type JsonDraftResult =
8
+ { ok: true; value: unknown } | { ok: false; error: string };
9
+
10
+ /** Pretty-print a value into the editor. Null/undefined → empty editor. */
11
+ export function formatJsonValue(value: unknown): string {
12
+ if (value === null || value === undefined) return "";
13
+ try {
14
+ return JSON.stringify(value, null, 2) ?? "";
15
+ } catch {
16
+ // Circular or non-serializable host value — start the editor empty
17
+ // rather than exploding the form.
18
+ return "";
19
+ }
20
+ }
21
+
22
+ /** Evaluate the operator's raw text. */
23
+ export function evaluateJsonDraft(text: string): JsonDraftResult {
24
+ const trimmed = text.trim();
25
+ if (!trimmed) return { ok: true, value: null };
26
+ try {
27
+ return { ok: true, value: JSON.parse(trimmed) };
28
+ } catch (parseError) {
29
+ const detail =
30
+ parseError instanceof Error ? parseError.message : String(parseError);
31
+ return { ok: false, error: `Invalid JSON: ${detail}` };
32
+ }
33
+ }
@@ -38,15 +38,27 @@ export declare function isIsoTimestamp(value: unknown): value is string;
38
38
  /** Locale date for an ISO timestamp (date-only, UTC-stable so SSR == hydrate);
39
39
  * the raw string when unparseable. */
40
40
  export declare function formatTimestamp(value: string, locale: string): string;
41
+ /** Locale date+time for an ISO timestamp (UTC-stable so SSR == hydrate); the
42
+ * raw string when unparseable. Unlike `formatTimestamp` (date-only), this keeps
43
+ * the time — for a schema `datetime` field the staff admin renders the full
44
+ * instant (incl. 00:00), since a datetime's time is meaningful data (SLA
45
+ * stamps, acknowledgements). A `date`-typed field uses `formatTimestamp`. */
46
+ export declare function formatDateTime(value: string, locale: string): string;
41
47
  /**
42
48
  * The single display boundary for list cells / detail values. Formats a value
43
- * as a locale date when it's a timestamp — either because `opts.treatAsDate`
44
- * forces it (the admin, from the schema field type) or because the value itself
45
- * looks like an ISO timestamp (the portal, value-shape). Everything else goes
46
- * through `formatScalar` (with the host's object-fallback policy).
49
+ * as a locale date/datetime when it's a timestamp — the caller drives which
50
+ * from the schema FIELD TYPE (not a value-shape guess):
51
+ * - `treatAsDate` → date-only (a `date`-typed field)
52
+ * - `treatAsDateTime` date+time (a `datetime`-typed field; time is data)
53
+ * - neither → value-shape: an ISO-timestamp STRING renders
54
+ * date-only (the portal, whose public schema strips
55
+ * typed created_at/updated_at). Unchanged.
56
+ * Everything else goes through `formatScalar` (with the host's object policy).
57
+ * `treatAsDate` wins if a caller mistakenly sets both.
47
58
  */
48
59
  export declare function displayCell(value: unknown, locale: string, opts?: {
49
60
  treatAsDate?: boolean;
61
+ treatAsDateTime?: boolean;
50
62
  objectFallback?: ObjectFallback;
51
63
  }): string;
52
64
  /** Operator copy for a status value (falls back to the raw value). `labels` is
@@ -92,20 +92,49 @@ export function formatTimestamp(value: string, locale: string): string {
92
92
  }
93
93
  }
94
94
 
95
+ /** Locale date+time for an ISO timestamp (UTC-stable so SSR == hydrate); the
96
+ * raw string when unparseable. Unlike `formatTimestamp` (date-only), this keeps
97
+ * the time — for a schema `datetime` field the staff admin renders the full
98
+ * instant (incl. 00:00), since a datetime's time is meaningful data (SLA
99
+ * stamps, acknowledgements). A `date`-typed field uses `formatTimestamp`. */
100
+ export function formatDateTime(value: string, locale: string): string {
101
+ const parsed = new Date(value);
102
+ if (Number.isNaN(parsed.getTime())) return value;
103
+ try {
104
+ return new Intl.DateTimeFormat(locale || "en", {
105
+ dateStyle: "medium",
106
+ timeStyle: "short",
107
+ timeZone: "UTC",
108
+ }).format(parsed);
109
+ } catch {
110
+ return value;
111
+ }
112
+ }
113
+
95
114
  /**
96
115
  * The single display boundary for list cells / detail values. Formats a value
97
- * as a locale date when it's a timestamp — either because `opts.treatAsDate`
98
- * forces it (the admin, from the schema field type) or because the value itself
99
- * looks like an ISO timestamp (the portal, value-shape). Everything else goes
100
- * through `formatScalar` (with the host's object-fallback policy).
116
+ * as a locale date/datetime when it's a timestamp — the caller drives which
117
+ * from the schema FIELD TYPE (not a value-shape guess):
118
+ * - `treatAsDate` → date-only (a `date`-typed field)
119
+ * - `treatAsDateTime` date+time (a `datetime`-typed field; time is data)
120
+ * - neither → value-shape: an ISO-timestamp STRING renders
121
+ * date-only (the portal, whose public schema strips
122
+ * typed created_at/updated_at). Unchanged.
123
+ * Everything else goes through `formatScalar` (with the host's object policy).
124
+ * `treatAsDate` wins if a caller mistakenly sets both.
101
125
  */
102
126
  export function displayCell(
103
127
  value: unknown,
104
128
  locale: string,
105
- opts: { treatAsDate?: boolean; objectFallback?: ObjectFallback } = {},
129
+ opts: {
130
+ treatAsDate?: boolean;
131
+ treatAsDateTime?: boolean;
132
+ objectFallback?: ObjectFallback;
133
+ } = {},
106
134
  ): string {
107
- if (opts.treatAsDate && typeof value === "string" && value) {
108
- return formatTimestamp(value, locale);
135
+ if (typeof value === "string" && value) {
136
+ if (opts.treatAsDate) return formatTimestamp(value, locale);
137
+ if (opts.treatAsDateTime) return formatDateTime(value, locale);
109
138
  }
110
139
  if (isIsoTimestamp(value)) return formatTimestamp(value, locale);
111
140
  return formatScalar(value, { objectFallback: opts.objectFallback });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,18 @@
35
35
  "types": "./components/action-form-visibility.d.ts",
36
36
  "default": "./components/action-form-visibility.ts"
37
37
  },
38
+ "./components/action-form-renderer-relationships": {
39
+ "types": "./components/action-form-renderer-relationships.d.ts",
40
+ "default": "./components/action-form-renderer-relationships.ts"
41
+ },
42
+ "./components/action-form-renderer-widgets": {
43
+ "types": "./components/action-form-renderer-widgets.d.ts",
44
+ "default": "./components/action-form-renderer-widgets.ts"
45
+ },
46
+ "./components/json-editor-contract": {
47
+ "types": "./components/json-editor-contract.d.ts",
48
+ "default": "./components/json-editor-contract.ts"
49
+ },
38
50
  "./components/*.svelte": {
39
51
  "types": "./components/*.svelte.d.ts",
40
52
  "svelte": "./components/*.svelte",