@happyvertical/smrt-svelte 0.55.7 → 0.55.8
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.
- package/AGENTS.md +7 -44
- package/README.md +112 -1
- package/agents/browser-ai.md +47 -0
- package/agents/command-palette.md +85 -0
- package/agents/import-export.md +62 -0
- package/agents/overview-surfaces.md +255 -0
- package/agents/screens.md +57 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
- package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
- package/dist/components/command-palette/CommandPalette.svelte +75 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/context.d.ts +18 -0
- package/dist/components/command-palette/context.d.ts.map +1 -0
- package/dist/components/command-palette/context.js +38 -0
- package/dist/components/command-palette/controller.svelte.d.ts +70 -0
- package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/controller.svelte.js +379 -0
- package/dist/components/command-palette/hotkeys.d.ts +30 -0
- package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
- package/dist/components/command-palette/hotkeys.js +161 -0
- package/dist/components/command-palette/index.d.ts +19 -0
- package/dist/components/command-palette/index.d.ts.map +1 -0
- package/dist/components/command-palette/index.js +17 -0
- package/dist/components/command-palette/match.d.ts +38 -0
- package/dist/components/command-palette/match.d.ts.map +1 -0
- package/dist/components/command-palette/match.js +172 -0
- package/dist/components/command-palette/providers.d.ts +159 -0
- package/dist/components/command-palette/providers.d.ts.map +1 -0
- package/dist/components/command-palette/providers.js +345 -0
- package/dist/components/command-palette/rank.d.ts +20 -0
- package/dist/components/command-palette/rank.d.ts.map +1 -0
- package/dist/components/command-palette/rank.js +74 -0
- package/dist/components/command-palette/types.d.ts +119 -0
- package/dist/components/command-palette/types.d.ts.map +1 -0
- package/dist/components/command-palette/types.js +10 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/use-provider.svelte.js +23 -0
- package/dist/components/import-export/ImportExport.svelte +678 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.d.ts +35 -0
- package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.js +42 -0
- package/dist/components/import-export/csv.d.ts +60 -0
- package/dist/components/import-export/csv.d.ts.map +1 -0
- package/dist/components/import-export/csv.js +192 -0
- package/dist/components/import-export/download.d.ts +4 -0
- package/dist/components/import-export/download.d.ts.map +1 -0
- package/dist/components/import-export/download.js +17 -0
- package/dist/components/import-export/export.d.ts +25 -0
- package/dist/components/import-export/export.d.ts.map +1 -0
- package/dist/components/import-export/export.js +68 -0
- package/dist/components/import-export/fields.d.ts +48 -0
- package/dist/components/import-export/fields.d.ts.map +1 -0
- package/dist/components/import-export/fields.js +103 -0
- package/dist/components/import-export/index.d.ts +12 -0
- package/dist/components/import-export/index.d.ts.map +1 -0
- package/dist/components/import-export/index.js +9 -0
- package/dist/components/import-export/mapping.d.ts +12 -0
- package/dist/components/import-export/mapping.d.ts.map +1 -0
- package/dist/components/import-export/mapping.js +38 -0
- package/dist/components/import-export/props.d.ts +34 -0
- package/dist/components/import-export/props.d.ts.map +1 -0
- package/dist/components/import-export/props.js +1 -0
- package/dist/components/import-export/report.d.ts +13 -0
- package/dist/components/import-export/report.d.ts.map +1 -0
- package/dist/components/import-export/report.js +50 -0
- package/dist/components/import-export/runner.d.ts +17 -0
- package/dist/components/import-export/runner.d.ts.map +1 -0
- package/dist/components/import-export/runner.js +39 -0
- package/dist/components/import-export/types.d.ts +100 -0
- package/dist/components/import-export/types.d.ts.map +1 -0
- package/dist/components/import-export/types.js +6 -0
- package/dist/components/import-export/validate.d.ts +39 -0
- package/dist/components/import-export/validate.d.ts.map +1 -0
- package/dist/components/import-export/validate.js +268 -0
- package/dist/components/overview/OverviewGrid.svelte +579 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
- package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.d.ts +92 -0
- package/dist/components/overview/controller.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.js +336 -0
- package/dist/components/overview/format.d.ts +13 -0
- package/dist/components/overview/format.d.ts.map +1 -0
- package/dist/components/overview/format.js +45 -0
- package/dist/components/overview/grid-types.d.ts +52 -0
- package/dist/components/overview/grid-types.d.ts.map +1 -0
- package/dist/components/overview/grid-types.js +1 -0
- package/dist/components/overview/index.d.ts +24 -0
- package/dist/components/overview/index.d.ts.map +1 -0
- package/dist/components/overview/index.js +22 -0
- package/dist/components/overview/load.d.ts +30 -0
- package/dist/components/overview/load.d.ts.map +1 -0
- package/dist/components/overview/load.js +92 -0
- package/dist/components/overview/markdown.d.ts +53 -0
- package/dist/components/overview/markdown.d.ts.map +1 -0
- package/dist/components/overview/markdown.js +203 -0
- package/dist/components/overview/model.d.ts +100 -0
- package/dist/components/overview/model.d.ts.map +1 -0
- package/dist/components/overview/model.js +460 -0
- package/dist/components/overview/recipe-widgets.d.ts +77 -0
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
- package/dist/components/overview/recipe-widgets.js +151 -0
- package/dist/components/overview/registry.d.ts +45 -0
- package/dist/components/overview/registry.d.ts.map +1 -0
- package/dist/components/overview/registry.js +112 -0
- package/dist/components/overview/resize.d.ts +25 -0
- package/dist/components/overview/resize.d.ts.map +1 -0
- package/dist/components/overview/resize.js +41 -0
- package/dist/components/overview/schema.d.ts +34 -0
- package/dist/components/overview/schema.d.ts.map +1 -0
- package/dist/components/overview/schema.js +180 -0
- package/dist/components/overview/server.d.ts +14 -0
- package/dist/components/overview/server.d.ts.map +1 -0
- package/dist/components/overview/server.js +13 -0
- package/dist/components/overview/types.d.ts +235 -0
- package/dist/components/overview/types.d.ts.map +1 -0
- package/dist/components/overview/types.js +17 -0
- package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +44 -0
- package/dist/components/overview/widgets/core.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.js +222 -0
- package/dist/components/overview/widgets/data.d.ts +76 -0
- package/dist/components/overview/widgets/data.d.ts.map +1 -0
- package/dist/components/overview/widgets/data.js +40 -0
- package/dist/components/screens/DetailScreen.svelte +220 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/EditForm.svelte +313 -0
- package/dist/components/screens/EditForm.svelte.d.ts +5 -0
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
- package/dist/components/screens/ListScreen.svelte +191 -0
- package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
- package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecipeScreens.svelte +237 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
- package/dist/components/screens/fields.d.ts +54 -0
- package/dist/components/screens/fields.d.ts.map +1 -0
- package/dist/components/screens/fields.js +229 -0
- package/dist/components/screens/index.d.ts +8 -0
- package/dist/components/screens/index.d.ts.map +1 -0
- package/dist/components/screens/index.js +6 -0
- package/dist/components/screens/types.d.ts +215 -0
- package/dist/components/screens/types.d.ts.map +1 -0
- package/dist/components/screens/types.js +1 -0
- package/dist/components/screens/values.d.ts +62 -0
- package/dist/components/screens/values.d.ts.map +1 -0
- package/dist/components/screens/values.js +289 -0
- package/dist/components/sortable/controller.svelte.d.ts +10 -0
- package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
- package/dist/components/sortable/controller.svelte.js +29 -7
- package/dist/components/workspace/manifest-nav.d.ts +20 -0
- package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
- package/dist/components/workspace/manifest-nav.js +42 -13
- package/dist/i18n/server.d.ts +6 -0
- package/dist/i18n/server.d.ts.map +1 -1
- package/dist/i18n/server.js +6 -0
- package/dist/i18n/strings.import-export.d.ts +88 -0
- package/dist/i18n/strings.import-export.d.ts.map +1 -0
- package/dist/i18n/strings.import-export.js +88 -0
- package/dist/i18n/strings.overview.d.ts +100 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -0
- package/dist/i18n/strings.overview.js +107 -0
- package/dist/i18n/strings.palette.d.ts +18 -0
- package/dist/i18n/strings.palette.d.ts.map +1 -0
- package/dist/i18n/strings.palette.js +18 -0
- package/dist/i18n/strings.screens.d.ts +31 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -0
- package/dist/i18n/strings.screens.js +31 -0
- package/package.json +45 -16
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DetailScreen - a read-only record view for any model, derived from its
|
|
4
|
+
* manifest definition and resolved field policy (#3718).
|
|
5
|
+
*
|
|
6
|
+
* Basic-tier fields show first, grouped by the policy/manifest `group`;
|
|
7
|
+
* advanced-tier fields and the read-only created/updated timestamps sit
|
|
8
|
+
* behind a "More details" disclosure. Hidden fields and fields outside the
|
|
9
|
+
* policy never render, so a sensitive field the policy omits cannot leak
|
|
10
|
+
* through this screen.
|
|
11
|
+
*/
|
|
12
|
+
import { Alert, ConfirmDialog } from '@happyvertical/smrt-ui/feedback';
|
|
13
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
14
|
+
import { PageHeader } from '@happyvertical/smrt-ui/layout';
|
|
15
|
+
import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
|
|
16
|
+
import { M } from '../../i18n/strings.screens.js';
|
|
17
|
+
import {
|
|
18
|
+
deriveScreenFields,
|
|
19
|
+
groupScreenFields,
|
|
20
|
+
screenSourceError,
|
|
21
|
+
screenTitles,
|
|
22
|
+
} from './fields.js';
|
|
23
|
+
import type {
|
|
24
|
+
DetailScreenProps,
|
|
25
|
+
ScreenCollectionDefinition,
|
|
26
|
+
ScreenField,
|
|
27
|
+
ScreenPolicy,
|
|
28
|
+
ScreenRecord,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
import { formatScreenValue } from './values.js';
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
definition,
|
|
34
|
+
policy = null,
|
|
35
|
+
record,
|
|
36
|
+
title,
|
|
37
|
+
fields: include,
|
|
38
|
+
currency,
|
|
39
|
+
locale,
|
|
40
|
+
showHeader = true,
|
|
41
|
+
backLabel,
|
|
42
|
+
deleting = false,
|
|
43
|
+
onback,
|
|
44
|
+
onedit,
|
|
45
|
+
ondelete,
|
|
46
|
+
extraActions,
|
|
47
|
+
children,
|
|
48
|
+
}: DetailScreenProps = $props();
|
|
49
|
+
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
|
|
52
|
+
const titles = $derived(screenTitles(definition));
|
|
53
|
+
const sourceError = $derived(screenSourceError(definition, policy));
|
|
54
|
+
|
|
55
|
+
const allFields = $derived(
|
|
56
|
+
sourceError
|
|
57
|
+
? []
|
|
58
|
+
: deriveScreenFields(definition, policy, { mode: 'view', include }),
|
|
59
|
+
);
|
|
60
|
+
const primary = $derived(allFields.filter((f) => f.tier === 'basic'));
|
|
61
|
+
const secondary = $derived(allFields.filter((f) => f.tier === 'advanced'));
|
|
62
|
+
|
|
63
|
+
const heading = $derived.by(() => {
|
|
64
|
+
if (title) return title;
|
|
65
|
+
const label = allFields.find((f) => f.kind === 'text' && f.tier === 'basic');
|
|
66
|
+
const value = label ? record[label.name] : undefined;
|
|
67
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
68
|
+
? value
|
|
69
|
+
: titles.singular;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const formatContext = $derived({
|
|
73
|
+
currency,
|
|
74
|
+
locale,
|
|
75
|
+
yes: t(M['ui.screens.yes']),
|
|
76
|
+
no: t(M['ui.screens.no']),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function display(field: ScreenField): string {
|
|
80
|
+
return formatScreenValue(field, record[field.name], formatContext);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Only http(s) URLs become links; anything else renders as text. */
|
|
84
|
+
function safeHref(field: ScreenField, text: string): string | null {
|
|
85
|
+
if (field.kind === 'email') return `mailto:${text}`;
|
|
86
|
+
if (field.kind === 'url' && /^https?:\/\//i.test(text)) return text;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let confirmOpen = $state(false);
|
|
91
|
+
let deleteError = $state(false);
|
|
92
|
+
|
|
93
|
+
async function confirmDelete(): Promise<void> {
|
|
94
|
+
deleteError = false;
|
|
95
|
+
try {
|
|
96
|
+
await ondelete?.();
|
|
97
|
+
confirmOpen = false;
|
|
98
|
+
} catch {
|
|
99
|
+
deleteError = true;
|
|
100
|
+
confirmOpen = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
{#snippet fieldList(list: ScreenField[])}
|
|
106
|
+
{#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
|
|
107
|
+
<section class="smrt-detail-screen__group">
|
|
108
|
+
{#if group}<h2 class="smrt-detail-screen__group-title">{group}</h2>{/if}
|
|
109
|
+
<dl class="smrt-detail-screen__fields">
|
|
110
|
+
{#each groupFields as field (field.name)}
|
|
111
|
+
{@const text = display(field)}
|
|
112
|
+
<div class="smrt-detail-screen__field">
|
|
113
|
+
<dt>{field.label}</dt>
|
|
114
|
+
<dd>
|
|
115
|
+
{#if text === ''}
|
|
116
|
+
<span class="smrt-detail-screen__empty">{t(M['ui.screens.not_set'])}</span>
|
|
117
|
+
{:else if field.kind === 'json' || field.kind === 'textarea'}
|
|
118
|
+
<pre class="smrt-detail-screen__block">{text}</pre>
|
|
119
|
+
{:else if safeHref(field, text)}
|
|
120
|
+
<a href={safeHref(field, text)}>{text}</a>
|
|
121
|
+
{:else if field.kind === 'reference'}
|
|
122
|
+
<code>{text}</code>
|
|
123
|
+
{:else}
|
|
124
|
+
{text}
|
|
125
|
+
{/if}
|
|
126
|
+
</dd>
|
|
127
|
+
</div>
|
|
128
|
+
{/each}
|
|
129
|
+
</dl>
|
|
130
|
+
</section>
|
|
131
|
+
{/each}
|
|
132
|
+
{/snippet}
|
|
133
|
+
|
|
134
|
+
{#if showHeader}
|
|
135
|
+
<PageHeader title={heading}>
|
|
136
|
+
{#snippet actions()}
|
|
137
|
+
{#if onback}
|
|
138
|
+
<Button variant="ghost" onclick={onback}>
|
|
139
|
+
{backLabel ?? t(M['ui.screens.back'], { name: titles.plural.toLowerCase() })}
|
|
140
|
+
</Button>
|
|
141
|
+
{/if}
|
|
142
|
+
{#if extraActions}{@render extraActions()}{/if}
|
|
143
|
+
{#if onedit}
|
|
144
|
+
<Button variant="secondary" onclick={onedit}>{t(M['ui.screens.edit'])}</Button>
|
|
145
|
+
{/if}
|
|
146
|
+
{#if ondelete}
|
|
147
|
+
<Button variant="danger" onclick={() => (confirmOpen = true)}>
|
|
148
|
+
{t(M['ui.screens.delete'])}
|
|
149
|
+
</Button>
|
|
150
|
+
{/if}
|
|
151
|
+
{/snippet}
|
|
152
|
+
</PageHeader>
|
|
153
|
+
{/if}
|
|
154
|
+
|
|
155
|
+
<article class="smrt-detail-screen" aria-label={heading}>
|
|
156
|
+
{#if sourceError}
|
|
157
|
+
<Alert variant="error">{sourceError}</Alert>
|
|
158
|
+
{:else}
|
|
159
|
+
{#if deleteError}
|
|
160
|
+
<Alert variant="error" dismissible ondismiss={() => (deleteError = false)}>
|
|
161
|
+
{t(M['ui.screens.delete_failed'])}
|
|
162
|
+
</Alert>
|
|
163
|
+
{/if}
|
|
164
|
+
{@render fieldList(primary)}
|
|
165
|
+
{#if secondary.length > 0}
|
|
166
|
+
<Disclosure title={t(M['ui.screens.more_details'])}>
|
|
167
|
+
{@render fieldList(secondary)}
|
|
168
|
+
</Disclosure>
|
|
169
|
+
{/if}
|
|
170
|
+
{@render children?.()}
|
|
171
|
+
{/if}
|
|
172
|
+
</article>
|
|
173
|
+
|
|
174
|
+
{#if ondelete}
|
|
175
|
+
<ConfirmDialog
|
|
176
|
+
open={confirmOpen}
|
|
177
|
+
title={t(M['ui.screens.delete_title'], { name: titles.singular.toLowerCase() })}
|
|
178
|
+
message={t(M['ui.screens.delete_message'])}
|
|
179
|
+
confirmLabel={t(M['ui.screens.delete'])}
|
|
180
|
+
cancelLabel={t(M['ui.screens.cancel'])}
|
|
181
|
+
destructive
|
|
182
|
+
loading={deleting}
|
|
183
|
+
onconfirm={confirmDelete}
|
|
184
|
+
oncancel={() => (confirmOpen = false)}
|
|
185
|
+
/>
|
|
186
|
+
{/if}
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
.smrt-detail-screen {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
gap: var(--smrt-spacing-lg, 1.5rem);
|
|
193
|
+
}
|
|
194
|
+
.smrt-detail-screen__group-title {
|
|
195
|
+
margin: 0 0 var(--smrt-spacing-sm, 0.5rem);
|
|
196
|
+
font-size: var(--smrt-typography-title-medium-size, 1rem);
|
|
197
|
+
}
|
|
198
|
+
.smrt-detail-screen__fields {
|
|
199
|
+
display: grid;
|
|
200
|
+
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
|
|
201
|
+
gap: var(--smrt-spacing-md, 1rem);
|
|
202
|
+
margin: 0;
|
|
203
|
+
}
|
|
204
|
+
.smrt-detail-screen__field dt {
|
|
205
|
+
color: var(--smrt-color-on-surface-variant, #5f6368);
|
|
206
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
207
|
+
}
|
|
208
|
+
.smrt-detail-screen__field dd {
|
|
209
|
+
margin: 0;
|
|
210
|
+
overflow-wrap: anywhere;
|
|
211
|
+
}
|
|
212
|
+
.smrt-detail-screen__empty {
|
|
213
|
+
color: var(--smrt-color-on-surface-variant, #5f6368);
|
|
214
|
+
}
|
|
215
|
+
.smrt-detail-screen__block {
|
|
216
|
+
margin: 0;
|
|
217
|
+
white-space: pre-wrap;
|
|
218
|
+
font-family: inherit;
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DetailScreenProps } from './types.js';
|
|
2
|
+
declare const DetailScreen: import("svelte").Component<DetailScreenProps, {}, "">;
|
|
3
|
+
type DetailScreen = ReturnType<typeof DetailScreen>;
|
|
4
|
+
export default DetailScreen;
|
|
5
|
+
//# sourceMappingURL=DetailScreen.svelte.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EditForm - a create / edit form for any model, derived from its manifest
|
|
4
|
+
* definition and resolved field policy (#3718).
|
|
5
|
+
*
|
|
6
|
+
* Policy behavior: basic-tier fields render up front and advanced-tier fields
|
|
7
|
+
* sit behind an "Advanced fields" disclosure (opened automatically when one
|
|
8
|
+
* has a problem); hidden fields and fields outside the policy never render;
|
|
9
|
+
* policy labels, help and order win over the manifest; on create, resolved
|
|
10
|
+
* policy defaults prefill the draft. System fields are never editable.
|
|
11
|
+
*
|
|
12
|
+
* The form is transport-neutral. `onsubmit` receives a payload already
|
|
13
|
+
* converted to wire values (safe integers, integer minor units for money,
|
|
14
|
+
* ISO datetimes, parsed JSON); throw to report a failed save, or return
|
|
15
|
+
* `{ fieldErrors }` to attach server-side validation messages to fields.
|
|
16
|
+
* Remount with `{#key}` when switching to another record.
|
|
17
|
+
*
|
|
18
|
+
* For the policy gear, usage telemetry and per-field renderer registry use
|
|
19
|
+
* `ObjectForm` from `@happyvertical/smrt-fields/svelte` instead.
|
|
20
|
+
*/
|
|
21
|
+
import { Alert } from '@happyvertical/smrt-ui/feedback';
|
|
22
|
+
import {
|
|
23
|
+
Checkbox,
|
|
24
|
+
Form,
|
|
25
|
+
FormActionBar,
|
|
26
|
+
FormGroup,
|
|
27
|
+
Input,
|
|
28
|
+
Textarea,
|
|
29
|
+
} from '@happyvertical/smrt-ui/forms';
|
|
30
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
31
|
+
import { PageHeader } from '@happyvertical/smrt-ui/layout';
|
|
32
|
+
import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
|
|
33
|
+
import { untrack } from 'svelte';
|
|
34
|
+
import { M } from '../../i18n/strings.screens.js';
|
|
35
|
+
import {
|
|
36
|
+
deriveScreenFields,
|
|
37
|
+
groupScreenFields,
|
|
38
|
+
screenSourceError,
|
|
39
|
+
screenTitles,
|
|
40
|
+
} from './fields.js';
|
|
41
|
+
import type {
|
|
42
|
+
EditFormProps,
|
|
43
|
+
ScreenCollectionDefinition,
|
|
44
|
+
ScreenDraft,
|
|
45
|
+
ScreenField,
|
|
46
|
+
ScreenFieldErrorCode,
|
|
47
|
+
ScreenPolicy,
|
|
48
|
+
ScreenRecord,
|
|
49
|
+
} from './types.js';
|
|
50
|
+
import {
|
|
51
|
+
DEFAULT_SCREEN_CURRENCY,
|
|
52
|
+
draftForCreate,
|
|
53
|
+
draftFromRecord,
|
|
54
|
+
parseDraft,
|
|
55
|
+
} from './values.js';
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
definition,
|
|
59
|
+
policy = null,
|
|
60
|
+
record,
|
|
61
|
+
title,
|
|
62
|
+
fields: include,
|
|
63
|
+
currency = DEFAULT_SCREEN_CURRENCY,
|
|
64
|
+
showHeader = true,
|
|
65
|
+
submitting = false,
|
|
66
|
+
fieldErrors = {},
|
|
67
|
+
error = null,
|
|
68
|
+
onsubmit,
|
|
69
|
+
oncancel,
|
|
70
|
+
}: EditFormProps = $props();
|
|
71
|
+
|
|
72
|
+
const { t } = useI18n();
|
|
73
|
+
|
|
74
|
+
const isNew = $derived(record === undefined);
|
|
75
|
+
const titles = $derived(screenTitles(definition));
|
|
76
|
+
const sourceError = $derived(screenSourceError(definition, policy));
|
|
77
|
+
const heading = $derived(
|
|
78
|
+
title ??
|
|
79
|
+
t(M[isNew ? 'ui.screens.create_title' : 'ui.screens.edit_title'], {
|
|
80
|
+
name: titles.singular.toLowerCase(),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const allFields = $derived(
|
|
85
|
+
sourceError
|
|
86
|
+
? []
|
|
87
|
+
: deriveScreenFields(definition, policy, { mode: 'edit', include }),
|
|
88
|
+
);
|
|
89
|
+
const basicFields = $derived(allFields.filter((f) => f.tier === 'basic'));
|
|
90
|
+
const advancedFields = $derived(allFields.filter((f) => f.tier === 'advanced'));
|
|
91
|
+
|
|
92
|
+
function initialDraft(): ScreenDraft {
|
|
93
|
+
return record
|
|
94
|
+
? draftFromRecord(allFields, record, { currency })
|
|
95
|
+
: draftForCreate(allFields, policy, { currency });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let draft = $state<ScreenDraft>(initialDraft());
|
|
99
|
+
let localErrors = $state<Record<string, ScreenFieldErrorCode>>({});
|
|
100
|
+
let hostErrors = $state<Record<string, string>>({});
|
|
101
|
+
let failed = $state(false);
|
|
102
|
+
let working = $state(false);
|
|
103
|
+
let advancedOpen = $state(false);
|
|
104
|
+
|
|
105
|
+
// Re-seed when the host swaps the record (or the field set) under a mounted
|
|
106
|
+
// form; typing never re-seeds because only the identity is tracked.
|
|
107
|
+
const sessionKey = $derived(
|
|
108
|
+
`${definition.objectRef}:${record ? String(record[definition.idField ?? 'id']) : 'new'}:${allFields.map((f) => f.name).join(',')}`,
|
|
109
|
+
);
|
|
110
|
+
let appliedKey = untrack(() => sessionKey);
|
|
111
|
+
$effect(() => {
|
|
112
|
+
const key = sessionKey;
|
|
113
|
+
if (key === appliedKey) return;
|
|
114
|
+
appliedKey = key;
|
|
115
|
+
untrack(() => {
|
|
116
|
+
draft = initialDraft();
|
|
117
|
+
localErrors = {};
|
|
118
|
+
hostErrors = {};
|
|
119
|
+
failed = false;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function text(name: string): string {
|
|
124
|
+
const value = draft[name];
|
|
125
|
+
return typeof value === 'string' ? value : '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setText(name: string, value: string | number | undefined): void {
|
|
129
|
+
draft[name] = String(value ?? '');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function messageFor(field: ScreenField): string | undefined {
|
|
133
|
+
const code = localErrors[field.name];
|
|
134
|
+
if (code) return t(M[`ui.screens.error.${code}`]);
|
|
135
|
+
return fieldErrors[field.name] ?? hostErrors[field.name];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const advancedHasProblem = $derived(
|
|
139
|
+
advancedFields.some(
|
|
140
|
+
(f) => localErrors[f.name] || fieldErrors[f.name] || hostErrors[f.name],
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
function hint(field: ScreenField): string | undefined {
|
|
145
|
+
return field.help ?? undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function label(field: ScreenField): string {
|
|
149
|
+
return field.kind === 'money' ? `${field.label} (${currency})` : field.label;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function submit(): Promise<void> {
|
|
153
|
+
if (working || submitting) return;
|
|
154
|
+
failed = false;
|
|
155
|
+
hostErrors = {};
|
|
156
|
+
const parsed = parseDraft(allFields, draft, { currency, record });
|
|
157
|
+
localErrors = parsed.errors;
|
|
158
|
+
if (Object.keys(parsed.errors).length > 0) {
|
|
159
|
+
if (advancedFields.some((f) => parsed.errors[f.name])) advancedOpen = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
working = true;
|
|
163
|
+
try {
|
|
164
|
+
const result = await onsubmit(parsed.values, { isNew });
|
|
165
|
+
if (result?.fieldErrors) {
|
|
166
|
+
hostErrors = { ...result.fieldErrors };
|
|
167
|
+
if (advancedFields.some((f) => result.fieldErrors?.[f.name])) {
|
|
168
|
+
advancedOpen = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
failed = true;
|
|
173
|
+
} finally {
|
|
174
|
+
working = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const busy = $derived(working || submitting);
|
|
179
|
+
const formError = $derived(
|
|
180
|
+
error ?? (failed ? t(M['ui.screens.save_failed']) : null),
|
|
181
|
+
);
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
{#snippet control(field: ScreenField)}
|
|
185
|
+
{#if field.kind === 'boolean'}
|
|
186
|
+
<div class="smrt-edit-form__check">
|
|
187
|
+
<Checkbox
|
|
188
|
+
name={field.name}
|
|
189
|
+
label={field.label}
|
|
190
|
+
disabled={busy}
|
|
191
|
+
bind:checked={() => draft[field.name] === true, (v) => (draft[field.name] = v)}
|
|
192
|
+
/>
|
|
193
|
+
{#if field.help}
|
|
194
|
+
<small class="smrt-edit-form__hint">{field.help}</small>
|
|
195
|
+
{/if}
|
|
196
|
+
{#if messageFor(field)}
|
|
197
|
+
<small class="smrt-edit-form__error" role="alert">{messageFor(field)}</small>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
{:else}
|
|
201
|
+
<FormGroup
|
|
202
|
+
label={label(field)}
|
|
203
|
+
hint={hint(field)}
|
|
204
|
+
error={messageFor(field)}
|
|
205
|
+
required={field.required}
|
|
206
|
+
>
|
|
207
|
+
{#if field.kind === 'textarea' || field.kind === 'json'}
|
|
208
|
+
<Textarea
|
|
209
|
+
name={field.name}
|
|
210
|
+
rows={field.kind === 'json' ? 8 : 4}
|
|
211
|
+
disabled={busy}
|
|
212
|
+
required={field.required}
|
|
213
|
+
bind:value={() => text(field.name), (v) => setText(field.name, v)}
|
|
214
|
+
/>
|
|
215
|
+
{:else}
|
|
216
|
+
<Input
|
|
217
|
+
name={field.name}
|
|
218
|
+
type={field.kind === 'email'
|
|
219
|
+
? 'email'
|
|
220
|
+
: field.kind === 'url'
|
|
221
|
+
? 'url'
|
|
222
|
+
: field.kind === 'tel'
|
|
223
|
+
? 'tel'
|
|
224
|
+
: field.kind === 'integer' || field.kind === 'decimal'
|
|
225
|
+
? 'number'
|
|
226
|
+
: field.kind === 'datetime'
|
|
227
|
+
? 'datetime-local'
|
|
228
|
+
: 'text'}
|
|
229
|
+
step={field.kind === 'integer' ? '1' : field.kind === 'decimal' ? 'any' : undefined}
|
|
230
|
+
inputmode={field.kind === 'money' ? 'decimal' : undefined}
|
|
231
|
+
disabled={busy}
|
|
232
|
+
required={field.required}
|
|
233
|
+
bind:value={() => text(field.name), (v) => setText(field.name, v)}
|
|
234
|
+
/>
|
|
235
|
+
{/if}
|
|
236
|
+
</FormGroup>
|
|
237
|
+
{/if}
|
|
238
|
+
{/snippet}
|
|
239
|
+
|
|
240
|
+
{#snippet fieldList(list: ScreenField[])}
|
|
241
|
+
{#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
|
|
242
|
+
<fieldset class="smrt-edit-form__group">
|
|
243
|
+
{#if group}<legend>{group}</legend>{/if}
|
|
244
|
+
{#each groupFields as field (field.name)}
|
|
245
|
+
{@render control(field)}
|
|
246
|
+
{/each}
|
|
247
|
+
</fieldset>
|
|
248
|
+
{/each}
|
|
249
|
+
{/snippet}
|
|
250
|
+
|
|
251
|
+
{#if showHeader}
|
|
252
|
+
<PageHeader title={heading} />
|
|
253
|
+
{/if}
|
|
254
|
+
|
|
255
|
+
<section class="smrt-edit-form" aria-label={heading}>
|
|
256
|
+
{#if sourceError}
|
|
257
|
+
<Alert variant="error">{sourceError}</Alert>
|
|
258
|
+
{:else}
|
|
259
|
+
<Form onsubmit={submit} novalidate>
|
|
260
|
+
{#if formError}
|
|
261
|
+
<Alert variant="error">{formError}</Alert>
|
|
262
|
+
{/if}
|
|
263
|
+
{@render fieldList(basicFields)}
|
|
264
|
+
{#if advancedFields.length > 0}
|
|
265
|
+
<Disclosure
|
|
266
|
+
title={t(M['ui.screens.advanced_fields'])}
|
|
267
|
+
bind:open={() => advancedOpen || advancedHasProblem, (v) => (advancedOpen = v)}
|
|
268
|
+
>
|
|
269
|
+
{@render fieldList(advancedFields)}
|
|
270
|
+
</Disclosure>
|
|
271
|
+
{/if}
|
|
272
|
+
<FormActionBar>
|
|
273
|
+
{#if oncancel}
|
|
274
|
+
<Button type="button" variant="ghost" disabled={busy} onclick={oncancel}>
|
|
275
|
+
{t(M['ui.screens.cancel'])}
|
|
276
|
+
</Button>
|
|
277
|
+
{/if}
|
|
278
|
+
<Button type="submit" variant="primary" loading={busy} disabled={busy}>
|
|
279
|
+
{t(M['ui.screens.save'])}
|
|
280
|
+
</Button>
|
|
281
|
+
</FormActionBar>
|
|
282
|
+
</Form>
|
|
283
|
+
{/if}
|
|
284
|
+
</section>
|
|
285
|
+
|
|
286
|
+
<style>
|
|
287
|
+
.smrt-edit-form {
|
|
288
|
+
max-width: 40rem;
|
|
289
|
+
}
|
|
290
|
+
.smrt-edit-form__group {
|
|
291
|
+
border: 0;
|
|
292
|
+
margin: 0 0 var(--smrt-spacing-md, 1rem);
|
|
293
|
+
padding: 0;
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-direction: column;
|
|
296
|
+
gap: var(--smrt-spacing-md, 1rem);
|
|
297
|
+
}
|
|
298
|
+
.smrt-edit-form__group legend {
|
|
299
|
+
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
300
|
+
margin-bottom: var(--smrt-spacing-sm, 0.5rem);
|
|
301
|
+
}
|
|
302
|
+
.smrt-edit-form__check {
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-direction: column;
|
|
305
|
+
gap: var(--smrt-spacing-xs, 0.25rem);
|
|
306
|
+
}
|
|
307
|
+
.smrt-edit-form__hint {
|
|
308
|
+
color: var(--smrt-color-on-surface-variant, #5f6368);
|
|
309
|
+
}
|
|
310
|
+
.smrt-edit-form__error {
|
|
311
|
+
color: var(--smrt-color-error, #b42318);
|
|
312
|
+
}
|
|
313
|
+
</style>
|
|
@@ -0,0 +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"}
|