@happyvertical/smrt-svelte 0.55.23 → 0.56.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.
Files changed (108) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +2 -2
  3. package/agents/overview-surfaces.md +117 -6
  4. package/agents/screens.md +171 -5
  5. package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
  6. package/dist/components/overview/OverviewGrid.svelte +8 -4
  7. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
  8. package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
  9. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
  10. package/dist/components/overview/format.d.ts +3 -12
  11. package/dist/components/overview/format.d.ts.map +1 -1
  12. package/dist/components/overview/format.js +2 -45
  13. package/dist/components/overview/grid-types.d.ts +6 -11
  14. package/dist/components/overview/grid-types.d.ts.map +1 -1
  15. package/dist/components/overview/index.d.ts +5 -1
  16. package/dist/components/overview/index.d.ts.map +1 -1
  17. package/dist/components/overview/index.js +3 -0
  18. package/dist/components/overview/recipe-widgets.d.ts +6 -3
  19. package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
  20. package/dist/components/overview/recipe-widgets.js +9 -0
  21. package/dist/components/overview/record.d.ts +51 -0
  22. package/dist/components/overview/record.d.ts.map +1 -0
  23. package/dist/components/overview/record.js +146 -0
  24. package/dist/components/overview/registry.d.ts +3 -0
  25. package/dist/components/overview/registry.d.ts.map +1 -1
  26. package/dist/components/overview/registry.js +25 -0
  27. package/dist/components/overview/server.d.ts +5 -1
  28. package/dist/components/overview/server.d.ts.map +1 -1
  29. package/dist/components/overview/server.js +5 -1
  30. package/dist/components/overview/types.d.ts +29 -0
  31. package/dist/components/overview/types.d.ts.map +1 -1
  32. package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
  33. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
  34. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -1
  35. package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
  36. package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
  37. package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
  38. package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
  39. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
  40. package/dist/components/overview/widgets/RecordTable.svelte +167 -0
  41. package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
  42. package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
  43. package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
  44. package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
  45. package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
  46. package/dist/components/overview/widgets/core.d.ts +13 -2
  47. package/dist/components/overview/widgets/core.d.ts.map +1 -1
  48. package/dist/components/overview/widgets/core.js +43 -2
  49. package/dist/components/overview/widgets/data.d.ts +97 -8
  50. package/dist/components/overview/widgets/data.d.ts.map +1 -1
  51. package/dist/components/overview/widgets/line-totals.d.ts +48 -0
  52. package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
  53. package/dist/components/overview/widgets/line-totals.js +91 -0
  54. package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
  55. package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
  56. package/dist/components/overview/widgets/record-loaders.js +150 -0
  57. package/dist/components/overview/widgets/record-table.d.ts +45 -0
  58. package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
  59. package/dist/components/overview/widgets/record-table.js +201 -0
  60. package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
  61. package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
  62. package/dist/components/overview/widgets/record-widgets.js +100 -0
  63. package/dist/components/screens/DetailScreen.svelte +105 -6
  64. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
  65. package/dist/components/screens/EditForm.svelte +39 -3
  66. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
  67. package/dist/components/screens/RecipeScreens.svelte +18 -18
  68. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
  69. package/dist/components/screens/RecordHeader.svelte +238 -0
  70. package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
  71. package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
  72. package/dist/components/screens/RecordScreen.svelte +511 -0
  73. package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
  74. package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
  75. package/dist/components/screens/columns.d.ts +29 -0
  76. package/dist/components/screens/columns.d.ts.map +1 -0
  77. package/dist/components/screens/columns.js +115 -0
  78. package/dist/components/screens/fields.d.ts +12 -2
  79. package/dist/components/screens/fields.d.ts.map +1 -1
  80. package/dist/components/screens/fields.js +37 -5
  81. package/dist/components/screens/index.d.ts +7 -2
  82. package/dist/components/screens/index.d.ts.map +1 -1
  83. package/dist/components/screens/index.js +6 -1
  84. package/dist/components/screens/record-header.d.ts +51 -0
  85. package/dist/components/screens/record-header.d.ts.map +1 -0
  86. package/dist/components/screens/record-header.js +118 -0
  87. package/dist/components/screens/relations.d.ts +49 -0
  88. package/dist/components/screens/relations.d.ts.map +1 -0
  89. package/dist/components/screens/relations.js +98 -0
  90. package/dist/components/screens/roles.d.ts +56 -0
  91. package/dist/components/screens/roles.d.ts.map +1 -0
  92. package/dist/components/screens/roles.js +174 -0
  93. package/dist/components/screens/types.d.ts +182 -1
  94. package/dist/components/screens/types.d.ts.map +1 -1
  95. package/dist/components/screens/values.d.ts +23 -1
  96. package/dist/components/screens/values.d.ts.map +1 -1
  97. package/dist/components/screens/values.js +80 -4
  98. package/dist/components/workspace/admin-shell/AdminShell.svelte +4 -4
  99. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
  100. package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
  101. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
  102. package/dist/i18n/strings.overview.d.ts +25 -0
  103. package/dist/i18n/strings.overview.d.ts.map +1 -1
  104. package/dist/i18n/strings.overview.js +25 -0
  105. package/dist/i18n/strings.screens.d.ts +8 -0
  106. package/dist/i18n/strings.screens.d.ts.map +1 -1
  107. package/dist/i18n/strings.screens.js +8 -0
  108. package/package.json +16 -16
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The record-page widget definitions (#3718 record page): `related` and
3
+ * `line-items` (the contract's `lineItems`), registered per record view by `registerRecordWidgets`
4
+ * because they are built from the record's relations. Loaders and the
5
+ * loader contract are in `record-loaders.ts`.
6
+ */
7
+ import { defaultWidgetRegistry } from '../registry.js';
8
+ import LineItemsWidget from './LineItemsWidget.svelte';
9
+ import RelatedWidget from './RelatedWidget.svelte';
10
+ import { createLineItemsLoader, createRelatedLoader, LINE_ITEMS_LIMIT, RELATED_WIDGET_LIMIT, } from './record-loaders.js';
11
+ /** `related`: tabbed related records. Options: `title`, `relations`, `limit`. */
12
+ export function createRelatedWidget(load) {
13
+ return {
14
+ type: 'related',
15
+ title: 'ui.overview.widget.related.title',
16
+ description: 'ui.overview.widget.related.description',
17
+ icon: 'layers',
18
+ options: [
19
+ {
20
+ key: 'title',
21
+ type: 'text',
22
+ label: 'ui.overview.opt.title',
23
+ help: 'ui.overview.opt.title_help',
24
+ maxLength: 80,
25
+ },
26
+ {
27
+ key: 'relations',
28
+ type: 'text',
29
+ label: 'ui.overview.opt.relations',
30
+ help: 'ui.overview.opt.relations_help',
31
+ maxLength: 1000,
32
+ },
33
+ {
34
+ key: 'limit',
35
+ type: 'integer',
36
+ label: 'ui.overview.opt.limit',
37
+ default: RELATED_WIDGET_LIMIT,
38
+ min: 1,
39
+ max: 20,
40
+ },
41
+ ],
42
+ component: RelatedWidget,
43
+ load,
44
+ defaultSpan: 4,
45
+ };
46
+ }
47
+ /**
48
+ * `line-items`: a document's lines and totals block. Options: `title`,
49
+ * `relation` (an `items` relation key; empty = the first), `limit`. The
50
+ * type id is kebab case (`line-items`): widget type ids are lowercase kebab.
51
+ */
52
+ export function createLineItemsWidget(load) {
53
+ return {
54
+ type: 'line-items',
55
+ title: 'ui.overview.widget.line_items.title',
56
+ description: 'ui.overview.widget.line_items.description',
57
+ icon: 'fileText',
58
+ options: [
59
+ {
60
+ key: 'title',
61
+ type: 'text',
62
+ label: 'ui.overview.opt.title',
63
+ help: 'ui.overview.opt.title_help',
64
+ maxLength: 80,
65
+ },
66
+ {
67
+ key: 'relation',
68
+ type: 'identifier',
69
+ label: 'ui.overview.opt.relation',
70
+ help: 'ui.overview.opt.relation_help',
71
+ },
72
+ {
73
+ key: 'limit',
74
+ type: 'integer',
75
+ label: 'ui.overview.opt.limit',
76
+ default: LINE_ITEMS_LIMIT,
77
+ min: 1,
78
+ max: LINE_ITEMS_LIMIT,
79
+ },
80
+ ],
81
+ component: LineItemsWidget,
82
+ load,
83
+ defaultSpan: 4,
84
+ };
85
+ }
86
+ /**
87
+ * Register the record-page widgets for one record view's relations,
88
+ * `related` and `line-items`, both loading through the host's `records`
89
+ * loader. Returns a disposer.
90
+ */
91
+ export function registerRecordWidgets(registry = defaultWidgetRegistry, options, registerOptions = {}) {
92
+ const disposers = [
93
+ registry.register(createRelatedWidget(createRelatedLoader(options)), registerOptions),
94
+ registry.register(createLineItemsWidget(createLineItemsLoader(options)), registerOptions),
95
+ ];
96
+ return () => {
97
+ for (const dispose of disposers)
98
+ dispose();
99
+ };
100
+ }
@@ -8,6 +8,12 @@
8
8
  * behind a "More details" disclosure. Hidden fields and fields outside the
9
9
  * policy never render, so a sensitive field the policy omits cannot leak
10
10
  * through this screen.
11
+ *
12
+ * Empty values (an empty JSON object or array included) show the "Not set"
13
+ * placeholder; a JSON object shows as a compact key/value list. A reference
14
+ * shows its target's label from `labels` (a link when `hrefFor` gives one);
15
+ * without a label it falls back to a shortened id in `<code>`, the full id in
16
+ * its tooltip.
11
17
  */
12
18
  import { Alert, ConfirmDialog } from '@happyvertical/smrt-ui/feedback';
13
19
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
@@ -17,6 +23,7 @@ import { M } from '../../i18n/strings.screens.js';
17
23
  import {
18
24
  deriveScreenFields,
19
25
  groupScreenFields,
26
+ humanizeFieldName,
20
27
  screenSourceError,
21
28
  screenTitles,
22
29
  } from './fields.js';
@@ -27,7 +34,11 @@ import type {
27
34
  ScreenPolicy,
28
35
  ScreenRecord,
29
36
  } from './types.js';
30
- import { formatScreenValue } from './values.js';
37
+ import {
38
+ formatScreenValue,
39
+ jsonScreenEntries,
40
+ shortScreenId,
41
+ } from './values.js';
31
42
 
32
43
  let {
33
44
  definition,
@@ -45,6 +56,12 @@ let {
45
56
  ondelete,
46
57
  extraActions,
47
58
  children,
59
+ relations = [],
60
+ hrefFor,
61
+ labels,
62
+ exclude,
63
+ hideEmpty = false,
64
+ variant = 'plain',
48
65
  }: DetailScreenProps = $props();
49
66
 
50
67
  const { t } = useI18n();
@@ -57,8 +74,17 @@ const allFields = $derived(
57
74
  ? []
58
75
  : deriveScreenFields(definition, policy, { mode: 'view', include }),
59
76
  );
60
- const primary = $derived(allFields.filter((f) => f.tier === 'basic'));
61
- const secondary = $derived(allFields.filter((f) => f.tier === 'advanced'));
77
+ const listed = $derived(
78
+ exclude ? allFields.filter((f) => !exclude.includes(f.name)) : allFields,
79
+ );
80
+ const filled = $derived(
81
+ hideEmpty ? listed.filter((f) => display(f) !== '') : listed,
82
+ );
83
+ const empty = $derived(
84
+ hideEmpty ? listed.filter((f) => display(f) === '') : [],
85
+ );
86
+ const primary = $derived(filled.filter((f) => f.tier === 'basic'));
87
+ const secondary = $derived(filled.filter((f) => f.tier === 'advanced'));
62
88
 
63
89
  const heading = $derived.by(() => {
64
90
  if (title) return title;
@@ -87,6 +113,19 @@ function safeHref(field: ScreenField, text: string): string | null {
87
113
  return null;
88
114
  }
89
115
 
116
+ /** A reference field's target model, from the `reference` relations. */
117
+ function targetModel(field: ScreenField): string | undefined {
118
+ return relations.find((r) => r.kind === 'reference' && r.field === field.name)
119
+ ?.model;
120
+ }
121
+
122
+ /** The referenced record's label, or `undefined` when nothing names it. */
123
+ function referenceLabel(field: ScreenField, id: string): string | undefined {
124
+ const model = targetModel(field);
125
+ const label = model ? labels?.[model]?.(id) : undefined;
126
+ return label && label.trim() !== '' ? label : undefined;
127
+ }
128
+
90
129
  let confirmOpen = $state(false);
91
130
  let deleteError = $state(false);
92
131
 
@@ -102,6 +141,8 @@ async function confirmDelete(): Promise<void> {
102
141
  }
103
142
  </script>
104
143
 
144
+ {#snippet shortId(id: string)}<code title={id}>{shortScreenId(id)}</code>{/snippet}
145
+
105
146
  {#snippet fieldList(list: ScreenField[])}
106
147
  {#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
107
148
  <section class="smrt-detail-screen__group">
@@ -114,12 +155,34 @@ async function confirmDelete(): Promise<void> {
114
155
  <dd>
115
156
  {#if text === ''}
116
157
  <span class="smrt-detail-screen__empty">{t(M['ui.screens.not_set'])}</span>
117
- {:else if field.kind === 'json' || field.kind === 'textarea'}
158
+ {:else if field.kind === 'json'}
159
+ {@const entries = jsonScreenEntries(record[field.name])}
160
+ {#if entries}
161
+ <dl class="smrt-detail-screen__pairs">
162
+ {#each entries as entry (entry.key)}
163
+ <div>
164
+ <dt>{humanizeFieldName(entry.key)}</dt>
165
+ <dd>{entry.value}</dd>
166
+ </div>
167
+ {/each}
168
+ </dl>
169
+ {:else}
170
+ <pre class="smrt-detail-screen__block">{text}</pre>
171
+ {/if}
172
+ {:else if field.kind === 'textarea'}
118
173
  <pre class="smrt-detail-screen__block">{text}</pre>
119
174
  {:else if safeHref(field, text)}
120
175
  <a href={safeHref(field, text)}>{text}</a>
121
176
  {:else if field.kind === 'reference'}
122
- <code>{text}</code>
177
+ {@const href = hrefFor?.(field, text)}
178
+ {@const label = referenceLabel(field, text)}
179
+ {#if href}
180
+ <a href={href} class="smrt-detail-screen__reference">{#if label}{label}{:else}{@render shortId(text)}{/if}</a>
181
+ {:else if label}
182
+ <span class="smrt-detail-screen__reference">{label}</span>
183
+ {:else}
184
+ {@render shortId(text)}
185
+ {/if}
123
186
  {:else}
124
187
  {text}
125
188
  {/if}
@@ -152,7 +215,12 @@ async function confirmDelete(): Promise<void> {
152
215
  </PageHeader>
153
216
  {/if}
154
217
 
155
- <article class="smrt-detail-screen" aria-label={heading} data-smrt-component="DetailScreen">
218
+ <article
219
+ class="smrt-detail-screen"
220
+ class:smrt-detail-screen--card={variant === 'card'}
221
+ aria-label={variant === 'card' ? t(M['ui.screens.details']) : heading}
222
+ data-smrt-component="DetailScreen"
223
+ >
156
224
  {#if sourceError}
157
225
  <Alert variant="error">{sourceError}</Alert>
158
226
  {:else}
@@ -167,6 +235,11 @@ async function confirmDelete(): Promise<void> {
167
235
  {@render fieldList(secondary)}
168
236
  </Disclosure>
169
237
  {/if}
238
+ {#if empty.length > 0}
239
+ <Disclosure title={t(M['ui.screens.show_empty'], { count: empty.length })}>
240
+ {@render fieldList(empty)}
241
+ </Disclosure>
242
+ {/if}
170
243
  {@render children?.()}
171
244
  {/if}
172
245
  </article>
@@ -193,6 +266,15 @@ async function confirmDelete(): Promise<void> {
193
266
  flex-direction: column;
194
267
  gap: var(--smrt-spacing-lg, 1.5rem);
195
268
  }
269
+ .smrt-detail-screen--card {
270
+ padding: var(--smrt-spacing-md, 1rem);
271
+ border: var(--smrt-hairline);
272
+ border-radius: var(--smrt-radius-lg);
273
+ background: var(--smrt-color-surface);
274
+ }
275
+ .smrt-detail-screen--card .smrt-detail-screen__fields {
276
+ grid-template-columns: repeat(auto-fill, minmax(max(14rem, 40%), 1fr));
277
+ }
196
278
  .smrt-detail-screen__group-title {
197
279
  margin: 0 0 var(--smrt-spacing-sm, 0.5rem);
198
280
  font-size: var(--smrt-typography-title-medium-size, 1rem);
@@ -214,6 +296,23 @@ async function confirmDelete(): Promise<void> {
214
296
  .smrt-detail-screen__empty {
215
297
  color: var(--smrt-color-on-surface-variant);
216
298
  }
299
+ .smrt-detail-screen__pairs {
300
+ display: grid;
301
+ gap: var(--smrt-spacing-1, 0.25rem);
302
+ margin: 0;
303
+ }
304
+ .smrt-detail-screen__pairs div {
305
+ display: flex;
306
+ flex-wrap: wrap;
307
+ gap: 0 var(--smrt-spacing-sm, 0.5rem);
308
+ }
309
+ .smrt-detail-screen__pairs dt {
310
+ color: var(--smrt-color-on-surface-variant);
311
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
312
+ }
313
+ .smrt-detail-screen__pairs dd {
314
+ margin: 0;
315
+ }
217
316
  .smrt-detail-screen__block {
218
317
  margin: 0;
219
318
  white-space: pre-wrap;
@@ -1 +1 @@
1
- {"version":3,"file":"DetailScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/DetailScreen.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAoKpB,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"DetailScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/DetailScreen.svelte.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAiOpB,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -25,6 +25,7 @@ import {
25
25
  FormActionBar,
26
26
  FormGroup,
27
27
  Input,
28
+ Select,
28
29
  Textarea,
29
30
  } from '@happyvertical/smrt-ui/forms';
30
31
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
@@ -32,6 +33,7 @@ import { PageHeader } from '@happyvertical/smrt-ui/layout';
32
33
  import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
33
34
  import { untrack } from 'svelte';
34
35
  import { M } from '../../i18n/strings.screens.js';
36
+ import { statusLabel } from '../overview/widgets/record-table.js';
35
37
  import {
36
38
  deriveScreenFields,
37
39
  groupScreenFields,
@@ -51,6 +53,7 @@ import {
51
53
  DEFAULT_SCREEN_CURRENCY,
52
54
  draftForCreate,
53
55
  draftFromRecord,
56
+ formatScreenValue,
54
57
  parseDraft,
55
58
  } from './values.js';
56
59
 
@@ -67,6 +70,7 @@ let {
67
70
  error = null,
68
71
  onsubmit,
69
72
  oncancel,
73
+ derivedFields = [],
70
74
  }: EditFormProps = $props();
71
75
 
72
76
  const { t } = useI18n();
@@ -86,6 +90,9 @@ const allFields = $derived(
86
90
  ? []
87
91
  : deriveScreenFields(definition, policy, { mode: 'edit', include }),
88
92
  );
93
+ /** Derived fields (a total computed from line items) show but never submit. */
94
+ const isDerived = (field: ScreenField): boolean =>
95
+ derivedFields.includes(field.name);
89
96
  const basicFields = $derived(allFields.filter((f) => f.tier === 'basic'));
90
97
  const advancedFields = $derived(allFields.filter((f) => f.tier === 'advanced'));
91
98
 
@@ -153,7 +160,11 @@ async function submit(): Promise<void> {
153
160
  if (working || submitting) return;
154
161
  failed = false;
155
162
  hostErrors = {};
156
- const parsed = parseDraft(allFields, draft, { currency, record });
163
+ const parsed = parseDraft(
164
+ allFields.filter((f) => !isDerived(f)),
165
+ draft,
166
+ { currency, record },
167
+ );
157
168
  localErrors = parsed.errors;
158
169
  if (Object.keys(parsed.errors).length > 0) {
159
170
  if (advancedFields.some((f) => parsed.errors[f.name])) advancedOpen = true;
@@ -182,7 +193,15 @@ const formError = $derived(
182
193
  </script>
183
194
 
184
195
  {#snippet control(field: ScreenField)}
185
- {#if field.kind === 'boolean'}
196
+ {#if isDerived(field)}
197
+ <FormGroup label={label(field)} hint={t(M['ui.screens.derived_hint'])}>
198
+ <Input
199
+ name={field.name}
200
+ readonly
201
+ value={formatScreenValue(field, record?.[field.name], { currency })}
202
+ />
203
+ </FormGroup>
204
+ {:else if field.kind === 'boolean'}
186
205
  <div class="smrt-edit-form__check">
187
206
  <Checkbox
188
207
  name={field.name}
@@ -204,7 +223,24 @@ const formError = $derived(
204
223
  error={messageFor(field)}
205
224
  required={field.required}
206
225
  >
207
- {#if field.kind === 'textarea' || field.kind === 'json'}
226
+ {#if field.kind === 'select'}
227
+ <Select
228
+ name={field.name}
229
+ disabled={busy}
230
+ required={field.required}
231
+ bind:value={() => text(field.name), (v) => setText(field.name, v)}
232
+ >
233
+ {#if !field.required || text(field.name) === ''}
234
+ <option value="">{t(M['ui.screens.select_none'])}</option>
235
+ {/if}
236
+ {#each field.options ?? [] as option (option)}
237
+ <option value={option}>{statusLabel(option)}</option>
238
+ {/each}
239
+ {#if text(field.name) !== '' && !(field.options ?? []).includes(text(field.name))}
240
+ <option value={text(field.name)}>{text(field.name)}</option>
241
+ {/if}
242
+ </Select>
243
+ {:else if field.kind === 'textarea' || field.kind === 'json'}
208
244
  <Textarea
209
245
  name={field.name}
210
246
  rows={field.kind === 'json' ? 8 : 4}
@@ -1 +1 @@
1
- {"version":3,"file":"EditForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/EditForm.svelte.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,YAAY,CAAC;AAmOpB,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"EditForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/EditForm.svelte.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,YAAY,CAAC;AA6PpB,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -5,7 +5,8 @@
5
5
  * screen set for a package that ships models but no consumer UI: tags, facts,
6
6
  * secrets, profiles, reports, prompts and the like.
7
7
  *
8
- * It composes `ListScreen`, `DetailScreen` and `EditForm` over a
8
+ * It composes `ListScreen`, `RecordScreen` (view and edit, the record view
9
+ * with its related-records overview) and `EditForm` (create) over a
9
10
  * transport-neutral `source` (list / get / create / update / delete). The
10
11
  * `view` and `recordId` props are bindable so an app can mirror them into its
11
12
  * router (`onnavigate` fires on every user navigation); left unbound, the
@@ -18,10 +19,11 @@ import { useI18n } from '@happyvertical/smrt-ui/i18n';
18
19
  import { Button } from '@happyvertical/smrt-ui/ui';
19
20
  import { untrack } from 'svelte';
20
21
  import { M } from '../../i18n/strings.screens.js';
21
- import DetailScreen from './DetailScreen.svelte';
22
22
  import EditForm from './EditForm.svelte';
23
23
  import { screenTitles } from './fields.js';
24
24
  import ListScreen from './ListScreen.svelte';
25
+ import RecordScreen from './RecordScreen.svelte';
26
+ import { deriveScreenRelations } from './relations.js';
25
27
  import type {
26
28
  RecipeScreensProps,
27
29
  RecipeScreensSource,
@@ -44,6 +46,8 @@ let {
44
46
  locale,
45
47
  can = {},
46
48
  onnavigate,
49
+ definitions,
50
+ record: recordProps = {},
47
51
  }: RecipeScreensProps = $props();
48
52
 
49
53
  const { t } = useI18n();
@@ -53,6 +57,9 @@ const idField = $derived(definition.idField ?? 'id');
53
57
  const allowCreate = $derived(can.create !== false && !!source.create);
54
58
  const allowUpdate = $derived(can.update !== false && !!source.update);
55
59
  const allowDelete = $derived(can.delete !== false && !!source.delete);
60
+ const relations = $derived(
61
+ definitions ? deriveScreenRelations(definitions, definition.objectRef) : [],
62
+ );
56
63
 
57
64
  let rows = $state<readonly ScreenRecord[]>([]);
58
65
  let loading = $state(true);
@@ -182,33 +189,26 @@ async function remove(): Promise<void> {
182
189
  />
183
190
  {:else if (view === 'view' || view === 'edit') && recordId !== null}
184
191
  {#if current}
185
- {#if view === 'edit' && allowUpdate}
186
- {#key recordId}
187
- <EditForm
188
- {definition}
189
- {policy}
190
- {currency}
191
- {fields}
192
- record={current}
193
- submitting={saving}
194
- onsubmit={update}
195
- oncancel={() => navigate('view', recordId)}
196
- />
197
- {/key}
198
- {:else}
199
- <DetailScreen
192
+ {#key definition.objectRef}
193
+ <RecordScreen
200
194
  {definition}
201
195
  {policy}
202
196
  {currency}
203
197
  {locale}
204
198
  {fields}
199
+ {relations}
205
200
  record={current}
201
+ mode={view === 'edit' && allowUpdate ? 'edit' : 'view'}
206
202
  {deleting}
203
+ submitting={saving}
207
204
  onback={() => navigate('list')}
208
205
  onedit={allowUpdate ? () => navigate('edit', recordId) : undefined}
206
+ oncancel={() => navigate('view', recordId)}
207
+ onsave={allowUpdate ? update : undefined}
209
208
  ondelete={allowDelete ? remove : undefined}
209
+ {...recordProps}
210
210
  />
211
- {/if}
211
+ {/key}
212
212
  {:else if !resolving}
213
213
  <Alert variant="warning">{t(M['ui.screens.not_found'])}</Alert>
214
214
  <Button variant="ghost" onclick={() => navigate('list')}>
@@ -1 +1 @@
1
- {"version":3,"file":"RecipeScreens.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecipeScreens.svelte.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,YAAY,CAAC;AAqLpB,QAAA,MAAM,aAAa,yEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"RecipeScreens.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecipeScreens.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,YAAY,CAAC;AAuLpB,QAAA,MAAM,aAAa,yEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}