includio-cms 0.0.8 → 0.0.9

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 (57) hide show
  1. package/dist/admin/client/admin/admin-after-login-layout.svelte +2 -9
  2. package/dist/admin/client/collection/collection-page.svelte +23 -19
  3. package/dist/admin/client/entry/entry-page.svelte +7 -3
  4. package/dist/admin/client/entry/entry.svelte +102 -39
  5. package/dist/admin/components/fields/array-field.svelte +88 -17
  6. package/dist/admin/components/fields/field-renderer.svelte +31 -28
  7. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -0
  8. package/dist/admin/components/fields/fields-form.svelte +7 -61
  9. package/dist/admin/components/fields/fields-form.svelte.d.ts +0 -3
  10. package/dist/admin/components/fields/object-field.svelte +27 -10
  11. package/dist/admin/components/fields/object-field.svelte.d.ts +1 -0
  12. package/dist/admin/components/fields/text-field-wrapper.svelte +4 -3
  13. package/dist/admin/components/tiptap.svelte +132 -45
  14. package/dist/components/ui/badge/badge.svelte +50 -0
  15. package/dist/components/ui/badge/badge.svelte.d.ts +32 -0
  16. package/dist/components/ui/badge/index.d.ts +2 -0
  17. package/dist/components/ui/badge/index.js +2 -0
  18. package/dist/components/ui/button-group/button-group-separator.svelte +18 -0
  19. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +13 -0
  20. package/dist/components/ui/button-group/button-group-text.svelte +33 -0
  21. package/dist/components/ui/button-group/button-group-text.svelte.d.ts +10 -0
  22. package/dist/components/ui/button-group/button-group.svelte +44 -0
  23. package/dist/components/ui/button-group/button-group.svelte.d.ts +25 -0
  24. package/dist/components/ui/button-group/index.d.ts +4 -0
  25. package/dist/components/ui/button-group/index.js +6 -0
  26. package/dist/components/ui/item/index.d.ts +11 -0
  27. package/dist/components/ui/item/index.js +13 -0
  28. package/dist/components/ui/item/item-actions.svelte +10 -0
  29. package/dist/components/ui/item/item-actions.svelte.d.ts +4 -0
  30. package/dist/components/ui/item/item-content.svelte +14 -0
  31. package/dist/components/ui/item/item-content.svelte.d.ts +4 -0
  32. package/dist/components/ui/item/item-description.svelte +22 -0
  33. package/dist/components/ui/item/item-description.svelte.d.ts +4 -0
  34. package/dist/components/ui/item/item-footer.svelte +14 -0
  35. package/dist/components/ui/item/item-footer.svelte.d.ts +4 -0
  36. package/dist/components/ui/item/item-group.svelte +15 -0
  37. package/dist/components/ui/item/item-group.svelte.d.ts +4 -0
  38. package/dist/components/ui/item/item-header.svelte +14 -0
  39. package/dist/components/ui/item/item-header.svelte.d.ts +4 -0
  40. package/dist/components/ui/item/item-media.svelte +40 -0
  41. package/dist/components/ui/item/item-media.svelte.d.ts +28 -0
  42. package/dist/components/ui/item/item-separator.svelte +14 -0
  43. package/dist/components/ui/item/item-separator.svelte.d.ts +13 -0
  44. package/dist/components/ui/item/item-title.svelte +14 -0
  45. package/dist/components/ui/item/item-title.svelte.d.ts +4 -0
  46. package/dist/components/ui/item/item.svelte +61 -0
  47. package/dist/components/ui/item/item.svelte.d.ts +46 -0
  48. package/dist/components/ui/separator/separator.svelte +2 -1
  49. package/dist/components/ui/toggle-group/index.d.ts +3 -0
  50. package/dist/components/ui/toggle-group/index.js +5 -0
  51. package/dist/components/ui/toggle-group/toggle-group-item.svelte +34 -0
  52. package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +6 -0
  53. package/dist/components/ui/toggle-group/toggle-group.svelte +47 -0
  54. package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +8 -0
  55. package/dist/db-postgres/index.js +3 -2
  56. package/dist/types/adapters.d.ts +2 -0
  57. package/package.json +10 -9
@@ -18,16 +18,9 @@
18
18
  style="--sidebar-width: calc(var(--spacing) * 72); --header-height: calc(var(--spacing) * 12);"
19
19
  >
20
20
  <AppSidebar variant="inset" />
21
+
21
22
  <Sidebar.Inset>
22
23
  <SiteHeader />
23
- <div class="flex flex-1 flex-col">
24
- <div class="@container/main flex flex-1 flex-col gap-2">
25
- <div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
26
- <div class="px-4 lg:px-6">
27
- {@render children()}
28
- </div>
29
- </div>
30
- </div>
31
- </div>
24
+ {@render children()}
32
25
  </Sidebar.Inset>
33
26
  </Sidebar.Provider>
@@ -31,23 +31,27 @@
31
31
  </script>
32
32
 
33
33
  {#if collection}
34
- <Button onclick={onCreateEntry} size="lg"><Plus /> Create new</Button>
35
-
36
- {#await collectionEntriesQuery then entries}
37
- {@const items = entries.map((entry) => ({
38
- id: entry.id,
39
- collection: entry.slug,
40
- name:
41
- collection.entryAdminTitle && entry.data[collection.entryAdminTitle]
42
- ? (entry.data[collection.entryAdminTitle] as Record<string, string>)[
43
- getContentLanguage()
44
- ] || entry.id
45
- : entry.id,
46
- url: `/admin/entries/${entry.id}`,
47
- status: entry.status,
48
- createdAt: entry.createdAt,
49
- updatedAt: entry.updatedAt
50
- }))}
51
- <DataTable data={items} {columns} />
52
- {/await}
34
+ <div class="p-4 md:p-6">
35
+ <Button onclick={onCreateEntry} size="lg" class="mb-4"><Plus /> Create new</Button>
36
+
37
+ {#await collectionEntriesQuery then entries}
38
+ {@const items = entries
39
+ .filter((entry) => entry.deletedAt === null)
40
+ .map((entry) => ({
41
+ id: entry.id,
42
+ collection: entry.slug,
43
+ name:
44
+ collection.entryAdminTitle && entry.data[collection.entryAdminTitle]
45
+ ? (entry.data[collection.entryAdminTitle] as Record<string, string>)[
46
+ getContentLanguage()
47
+ ] || entry.id
48
+ : entry.id,
49
+ url: `/admin/entries/${entry.id}`,
50
+ status: entry.status,
51
+ createdAt: entry.createdAt,
52
+ updatedAt: entry.updatedAt
53
+ }))}
54
+ <DataTable data={items} {columns} />
55
+ {/await}
56
+ </div>
53
57
  {/if}
@@ -6,9 +6,13 @@
6
6
 
7
7
  const remotes = getRemotes();
8
8
 
9
- let entry = $derived(await remotes.getEntryById(page.params.entryId || ''));
9
+ let entryQuery = $derived(remotes.getEntryById(page.params.entryId || ''));
10
10
  </script>
11
11
 
12
- {#key entry}
13
- <Entry {entry} />
12
+ {#key page.url}
13
+ {#await entryQuery}
14
+ Loading...
15
+ {:then entry}
16
+ <Entry {entry} />
17
+ {/await}
14
18
  {/key}
@@ -14,7 +14,15 @@
14
14
  import { zod, zodClient } from 'sveltekit-superforms/adapters';
15
15
 
16
16
  import { getRemotes } from '../../context/remotes.js';
17
- import { getContentLanguage } from '../../state/content-language.svelte.js';
17
+ import {
18
+ getContentLanguage,
19
+ setContentLanguage
20
+ } from '../../state/content-language.svelte.js';
21
+
22
+ import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
23
+ import * as ToggleGroup from '../../../components/ui/toggle-group/index.js';
24
+ import DotsVerticalIcon from '@tabler/icons-svelte/icons/dots-vertical';
25
+ import { page } from '$app/state';
18
26
 
19
27
  const remotes = getRemotes();
20
28
 
@@ -100,60 +108,115 @@
100
108
  });
101
109
  </script>
102
110
 
103
- <div class="flex items-stretch">
104
- <FieldsForm {form} fields={collection.fields} class="grow">
105
- {#snippet children()}
106
- <Button type="button" variant="destructive" class="mt-2 w-full" onclick={onDelete}
107
- >Delete</Button
108
- >
111
+ <div
112
+ class="bg-background sticky top-0 z-50 flex items-center justify-between gap-4 border-b px-4 py-3 md:px-6"
113
+ >
114
+ <div class="flex items-center gap-4">
115
+ <div class="flex items-center gap-1">
116
+ <p class="text-sm whitespace-nowrap">
117
+ <span class="text-muted-foreground">Status:</span>
118
+ <span class="capitalize">{entry.status}</span> -
119
+ </p>
109
120
 
110
- <p>Status: {entry.status}</p>
111
- <p>Created: {entry.createdAt.toLocaleString('pl')}</p>
112
- <p>Updated: {entry.updatedAt.toLocaleString('pl')}</p>
113
-
114
- {#if entry.publishedAt}
115
- <p>Published: {entry.publishedAt.toLocaleString('pl')}</p>
116
- {/if}
117
- <p class="text-muted-foreground text-xs">ID: {entry.id}</p>
118
- {/snippet}
119
-
120
- {#snippet after()}
121
121
  {#if entry.status === 'draft'}
122
- <Button
123
- onclick={() => {
124
- remotes.updateEntryCommand({
122
+ <button
123
+ onclick={async () => {
124
+ entry.status = 'published';
125
+ const updatedEntry = await remotes.updateEntryCommand({
125
126
  id: entry.id,
126
127
  data: { status: 'published' }
127
128
  });
129
+ entry = updatedEntry;
128
130
  }}
129
131
  type="button"
130
- class="mt-2 w-full">Publish</Button
132
+ class="text-sm underline">Publish</button
131
133
  >
132
134
  {:else}
133
- <Button
135
+ <button
134
136
  type="button"
135
- class="mt-2 w-full"
136
- onclick={() => {
137
- remotes.updateEntryCommand({
137
+ class="text-sm underline"
138
+ onclick={async () => {
139
+ entry.status = 'draft';
140
+ const updatedEntry = await remotes.updateEntryCommand({
138
141
  id: entry.id,
139
142
  data: { status: 'draft' }
140
143
  });
141
- }}>Unpublish</Button
144
+ entry = updatedEntry;
145
+ }}>Unpublish</button
142
146
  >
143
147
  {/if}
148
+ </div>
144
149
 
145
- {#if previewUrl}
146
- <Button
147
- type="button"
148
- onclick={() => {
149
- isPreview = !isPreview;
150
- }}
151
- class="mt-2 w-full"
152
- variant="outline">{isPreview ? 'Hide' : 'Show'} Preview</Button
153
- >
154
- {/if}
155
- {/snippet}
156
- </FieldsForm>
150
+ <p class="text-sm whitespace-nowrap">
151
+ <span class="text-muted-foreground">Modified:</span>
152
+ {entry.updatedAt.toLocaleString('pl')}
153
+ </p>
154
+ <p class="text-sm whitespace-nowrap">
155
+ <span class="text-muted-foreground">Created:</span>
156
+ {entry.createdAt.toLocaleString('pl')}
157
+ </p>
158
+ </div>
159
+
160
+ <div class="flex items-center gap-2">
161
+ {#if languages.length > 1}
162
+ <ToggleGroup.Root
163
+ size="sm"
164
+ type="single"
165
+ variant="outline"
166
+ onValueChange={(val) => {
167
+ if (val) {
168
+ setContentLanguage(val);
169
+ }
170
+ }}
171
+ value={getContentLanguage()}
172
+ >
173
+ {#each languages as lang}
174
+ <ToggleGroup.Item value={lang}>
175
+ {lang.toUpperCase()}
176
+ </ToggleGroup.Item>
177
+ {/each}
178
+ </ToggleGroup.Root>
179
+ {/if}
180
+
181
+ {#if previewUrl}
182
+ <Button
183
+ type="button"
184
+ size="sm"
185
+ onclick={() => {
186
+ isPreview = !isPreview;
187
+ }}
188
+ variant="outline">{isPreview ? 'Hide' : 'Show'} Preview</Button
189
+ >
190
+ {/if}
191
+
192
+ <Button
193
+ type="submit"
194
+ size="sm"
195
+ onclick={() => {
196
+ form.submit();
197
+ }}>Save</Button
198
+ >
199
+
200
+ <DropdownMenu.Root>
201
+ <DropdownMenu.Trigger class="data-[state=open]:bg-muted text-muted-foreground flex size-8">
202
+ {#snippet child({ props })}
203
+ <Button variant="ghost" size="icon" {...props}>
204
+ <DotsVerticalIcon />
205
+ <span class="sr-only">Open menu</span>
206
+ </Button>
207
+ {/snippet}
208
+ </DropdownMenu.Trigger>
209
+ <DropdownMenu.Content align="end" class="w-32">
210
+ <DropdownMenu.Item variant="destructive" onclick={onDelete}>Delete</DropdownMenu.Item>
211
+ </DropdownMenu.Content>
212
+ </DropdownMenu.Root>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="flex items-stretch">
217
+ <div class="grow p-4 lg:p-6">
218
+ <FieldsForm {form} fields={collection.fields} />
219
+ </div>
157
220
 
158
221
  {#if isPreview}
159
222
  <div class="flex w-[768px] shrink-0 flex-col space-y-4 pl-4">
@@ -20,6 +20,11 @@
20
20
  import { getContentLanguage } from '../../state/content-language.svelte.js';
21
21
  import FieldRenderer from './field-renderer.svelte';
22
22
  import { onMount, tick } from 'svelte';
23
+ import * as Form from '../../../components/ui/form/index.js';
24
+ import CirclePlus from '@tabler/icons-svelte/icons/circle-plus';
25
+ import Badge from '../../../components/ui/badge/badge.svelte';
26
+ import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
27
+ import DotsVerticalIcon from '@tabler/icons-svelte/icons/dots-vertical';
23
28
 
24
29
  type Props = {
25
30
  field: ArrayField;
@@ -55,13 +60,27 @@
55
60
  }
56
61
  ];
57
62
 
58
- await tick();
59
-
60
63
  openAndCloseOthers($value.length - 1);
61
64
  }
62
65
 
66
+ function duplicateItem(index: number) {
67
+ if (!$value) return;
68
+ const itemToDuplicate = $value[index];
69
+ if (!itemToDuplicate) return;
70
+
71
+ $value = [
72
+ ...$value.slice(0, index + 1),
73
+ JSON.parse(JSON.stringify(itemToDuplicate)),
74
+ ...$value.slice(index + 1)
75
+ ];
76
+
77
+ openAndCloseOthers(index + 1);
78
+ }
79
+
63
80
  function removeItem(index: number) {
64
81
  if (!$value) return;
82
+
83
+ accordionOpenState = accordionOpenState.filter((i) => i !== index.toString());
65
84
  $value = $value.filter((_, i) => i !== index);
66
85
  }
67
86
 
@@ -76,15 +95,15 @@
76
95
  const label = item.data[objectConfig.accordionLabelField] as string | Record<string, string>;
77
96
 
78
97
  if (typeof label === 'string' && label.trim().length > 0) {
79
- return `${label} (${objectConfig.label ?? objectConfig.slug})`;
98
+ return `${label}`;
80
99
  }
81
100
 
82
101
  if (typeof label === 'object' && label !== null) {
83
102
  const objectLabel = label as Record<string, string>;
84
- return `${objectLabel[getContentLanguage()]} (${objectConfig.label ?? objectConfig.slug})`;
103
+ return `${objectLabel[getContentLanguage()]}`;
85
104
  }
86
105
  }
87
- return objectConfig.label ?? objectConfig.slug;
106
+ return '';
88
107
  }
89
108
 
90
109
  let accordionOpenState = $state<string[]>([]);
@@ -94,7 +113,32 @@
94
113
  }
95
114
  </script>
96
115
 
97
- <Accordion.Root type="multiple" class="w-full" bind:value={accordionOpenState}>
116
+ <div class="flex items-center justify-between gap-4">
117
+ <Form.Label class="text-lg">{field.label}</Form.Label>
118
+
119
+ <div class="flex items-center gap-2">
120
+ <Button
121
+ size="sm"
122
+ type="button"
123
+ variant="ghost"
124
+ onclick={() => {
125
+ accordionOpenState = [];
126
+ }}>Collapse All</Button
127
+ >
128
+ <Button
129
+ size="sm"
130
+ type="button"
131
+ variant="ghost"
132
+ onclick={() => {
133
+ if ($value) {
134
+ accordionOpenState = $value.map((_, i) => i.toString());
135
+ }
136
+ }}>Show All</Button
137
+ >
138
+ </div>
139
+ </div>
140
+
141
+ <Accordion.Root type="multiple" class="w-full space-y-4" bind:value={accordionOpenState}>
98
142
  {#if $value && $value.length > 0}
99
143
  {#each $value as _, index (index)}
100
144
  {#if $value[index].data && $value[index].slug}
@@ -102,20 +146,46 @@
102
146
  {@const objectField = field.of.find((option) => option.slug === item.slug)}
103
147
 
104
148
  {#if objectField}
105
- <Accordion.Item value={index.toString()}>
106
- <Accordion.Trigger class="text-base font-normal">
107
- {getAccordionLabel($value[index])}
149
+ <Accordion.Item value={index.toString()} class="border-0">
150
+ <Accordion.Trigger
151
+ class="items-center border px-4 text-base font-normal data-[state=open]:rounded-b-none"
152
+ >
153
+ <div class="flex grow items-center justify-between gap-4">
154
+ <div class="flex items-center gap-4">
155
+ <span>{index < 10 ? '0' : ''}{index + 1}</span>
156
+ <Badge variant="outline">{objectField.label ?? objectField.slug}</Badge>
157
+ <span>{getAccordionLabel($value[index])}</span>
158
+ </div>
159
+
160
+ <DropdownMenu.Root>
161
+ <DropdownMenu.Trigger
162
+ class="data-[state=open]:bg-muted text-muted-foreground flex size-8"
163
+ >
164
+ {#snippet child({ props })}
165
+ <Button variant="ghost" size="icon" {...props}>
166
+ <DotsVerticalIcon />
167
+ <span class="sr-only">Open menu</span>
168
+ </Button>
169
+ {/snippet}
170
+ </DropdownMenu.Trigger>
171
+ <DropdownMenu.Content align="end" class="w-32">
172
+ <DropdownMenu.Item onclick={() => duplicateItem(index)}
173
+ >Duplicate</DropdownMenu.Item
174
+ >
175
+ <DropdownMenu.Item variant="destructive" onclick={() => removeItem(index)}
176
+ >Delete</DropdownMenu.Item
177
+ >
178
+ </DropdownMenu.Content>
179
+ </DropdownMenu.Root>
180
+ </div>
108
181
  </Accordion.Trigger>
109
- <Accordion.Content class="space-y-8">
182
+ <Accordion.Content class="space-y-4 rounded-b-md border border-t-0 p-4">
110
183
  <FieldRenderer
184
+ objectFieldType="inline"
111
185
  field={objectField}
112
186
  form={form as SuperForm<Record<string, unknown>>}
113
187
  path={joinPath(path, index) as FormPathLeaves<T, ObjectFieldData>}
114
188
  />
115
-
116
- <Button variant="destructive" type="button" onclick={() => removeItem(index)}
117
- >Delete</Button
118
- >
119
189
  </Accordion.Content>
120
190
  </Accordion.Item>
121
191
  {:else}
@@ -131,10 +201,11 @@
131
201
  {/if}
132
202
  </Accordion.Root>
133
203
 
134
- <div class="mt-2 flex flex-wrap gap-2">
204
+ <div class="mt-4 flex flex-wrap gap-2">
135
205
  {#each field.of as option}
136
- <Button size="sm" type="button" onclick={() => addItem(option)}>
137
- Add {option.label ?? option.slug}
206
+ <Button size="sm" type="button" variant="outline" onclick={() => addItem(option)}>
207
+ <CirclePlus />
208
+ {option.label ?? option.slug}
138
209
  </Button>
139
210
  {/each}
140
211
  </div>
@@ -13,15 +13,16 @@
13
13
  import FileField from './file-field.svelte';
14
14
 
15
15
  type Props = {
16
+ objectFieldType?: 'default' | 'inline';
16
17
  field: Field;
17
18
  form: SuperForm<Record<string, unknown>>;
18
19
  path: FormPathLeaves<Record<string, unknown>>;
19
20
  };
20
21
 
21
- let { field, form, path, ...props }: Props = $props();
22
+ let { field, form, path, objectFieldType = 'default', ...props }: Props = $props();
22
23
 
23
- const fieldsWithNoDescription: FieldType[] = ['boolean'];
24
- const fieldsWithNoLabel: FieldType[] = ['boolean'];
24
+ const fieldsWithNoDescription: FieldType[] = ['boolean', 'object', 'array'];
25
+ const fieldsWithNoLabel: FieldType[] = ['boolean', 'object', 'array'];
25
26
 
26
27
  const fieldsWithAlternativeDescription: FieldType[] = ['image', 'object', 'array'];
27
28
 
@@ -46,7 +47,7 @@
46
47
  <Form.FieldErrors />
47
48
  </Form.Fieldset>
48
49
  {:else if isTextField(field)}
49
- <Form.Field {form} name={path}>
50
+ <Form.Field {form} name={path} class="space-y-0">
50
51
  <TextFieldWrapper {field} {form} {path} {...props} />
51
52
  <Form.FieldErrors />
52
53
  </Form.Field>
@@ -54,34 +55,36 @@
54
55
  <Form.Field {form} name={path}>
55
56
  <Form.Control>
56
57
  {#snippet children({ props })}
57
- {#if field.label && !fieldsWithNoLabel.includes(field.type)}
58
- <Form.Label class={field.type === 'array' ? 'text-lg font-medium' : ''}>
59
- {field.label}
60
- </Form.Label>
61
- {/if}
58
+ <div class="space-y-2">
59
+ {#if field.label && !fieldsWithNoLabel.includes(field.type)}
60
+ <Form.Label class={field.type === 'array' ? 'text-lg font-medium' : ''}>
61
+ {field.label}
62
+ </Form.Label>
63
+ {/if}
62
64
 
63
- {#if !fieldsWithNoDescription.includes(field.type) && fieldsWithAlternativeDescription.includes(field.type)}
64
- <Form.Description>{field?.description ?? ''}</Form.Description>
65
- {/if}
65
+ {#if !fieldsWithNoDescription.includes(field.type) && fieldsWithAlternativeDescription.includes(field.type) && field.description}
66
+ <Form.Description>{field?.description ?? ''}</Form.Description>
67
+ {/if}
66
68
 
67
- {#if field.type === 'image'}
68
- <ImageField {field} {form} {path} {...props} />
69
- {:else if field.type === 'file'}
70
- <FileField {field} {form} {path} {...props} />
71
- {:else if field.type === 'array'}
72
- <ArrayField {field} {form} {path} {...props} />
73
- {:else if field.type === 'object'}
74
- <ObjectField {field} {form} {path} {...props} />
75
- {:else if field.type === 'slug'}
76
- <SlugField {field} {form} {path} {...props} />
77
- {:else if field.type === 'boolean'}
78
- <BooleanField {field} {form} {path} {...props} />
79
- {:else}
80
- <p>Nieobsługiwany typ pola: {field.type}</p>
81
- {/if}
69
+ {#if field.type === 'image'}
70
+ <ImageField {field} {form} {path} {...props} />
71
+ {:else if field.type === 'file'}
72
+ <FileField {field} {form} {path} {...props} />
73
+ {:else if field.type === 'array'}
74
+ <ArrayField {field} {form} {path} {...props} />
75
+ {:else if field.type === 'object'}
76
+ <ObjectField {field} {form} {path} {objectFieldType} {...props} />
77
+ {:else if field.type === 'slug'}
78
+ <SlugField {field} {form} {path} {...props} />
79
+ {:else if field.type === 'boolean'}
80
+ <BooleanField {field} {form} {path} {...props} />
81
+ {:else}
82
+ <p>Nieobsługiwany typ pola: {field.type}</p>
83
+ {/if}
84
+ </div>
82
85
  {/snippet}
83
86
  </Form.Control>
84
- {#if !fieldsWithNoDescription.includes(field.type) && !fieldsWithAlternativeDescription.includes(field.type)}
87
+ {#if !fieldsWithNoDescription.includes(field.type) && !fieldsWithAlternativeDescription.includes(field.type) && field.description}
85
88
  <Form.Description>{field?.description ?? ''}</Form.Description>
86
89
  {/if}
87
90
  <Form.FieldErrors />
@@ -1,6 +1,7 @@
1
1
  import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
2
2
  import type { Field } from '../../../types/fields.js';
3
3
  type Props = {
4
+ objectFieldType?: 'default' | 'inline';
4
5
  field: Field;
5
6
  form: SuperForm<Record<string, unknown>>;
6
7
  path: FormPathLeaves<Record<string, unknown>>;
@@ -1,78 +1,24 @@
1
1
  <script lang="ts">
2
2
  import { type SuperForm } from 'sveltekit-superforms/client';
3
-
4
3
  import type { Snippet } from 'svelte';
5
- import Button from '../../../components/ui/button/button.svelte';
6
4
  import { cn } from '../../../utils.js';
7
5
  import FieldRenderer from './field-renderer.svelte';
8
6
  import type { Field } from '../../../types/fields.js';
9
- import SuperDebug from 'sveltekit-superforms';
10
- import { dev } from '$app/environment';
11
7
 
12
8
  type Props = {
13
9
  form: SuperForm<Record<string, unknown>>;
14
10
  fields: Field[];
15
11
  action?: string | undefined;
16
- children?: Snippet;
17
- after?: Snippet;
18
12
  class?: string;
19
13
  };
20
14
 
21
- let {
22
- form,
23
- fields,
24
- action = undefined,
25
- children = undefined,
26
- after = undefined,
27
- class: className = undefined
28
- }: Props = $props();
15
+ let { form, fields, action = undefined, class: className = undefined }: Props = $props();
29
16
 
30
- const { enhance, allErrors, form: formData } = form;
17
+ const { enhance } = form;
31
18
  </script>
32
19
 
33
- <div>
34
- {#if dev && false}
35
- <div>
36
- <SuperDebug data={formData} />
37
-
38
- {#if $allErrors.length}
39
- <ul>
40
- {#each $allErrors as error}
41
- <li>
42
- <b>{error.path}:</b>
43
- {error.messages.join('. ')}
44
- </li>
45
- {/each}
46
- </ul>
47
- {/if}
48
- </div>
49
- {/if}
50
-
51
- <div class={cn('flex gap-4', className)}>
52
- <form method="POST" use:enhance {action} class="max-w-[calc(100%-300px)] grow space-y-8">
53
- {#each fields as field}
54
- <FieldRenderer {field} {form} path={field.slug} />
55
- {/each}
56
- </form>
57
-
58
- <div class="w-[300px] shrink-0">
59
- <div class="sticky top-4 space-y-2">
60
- <div class="rounded-2xl border p-4">
61
- <Button
62
- class="w-full"
63
- type="submit"
64
- onclick={() => {
65
- form.submit();
66
- }}>Save</Button
67
- >
68
- {@render children?.()}
69
- </div>
70
- {#if after}
71
- <div class="rounded-2xl border p-4">
72
- {@render after()}
73
- </div>
74
- {/if}
75
- </div>
76
- </div>
77
- </div>
78
- </div>
20
+ <form method="POST" use:enhance {action} class={cn('space-y-4', className)}>
21
+ {#each fields as field}
22
+ <FieldRenderer {field} {form} path={field.slug} />
23
+ {/each}
24
+ </form>
@@ -1,12 +1,9 @@
1
1
  import { type SuperForm } from 'sveltekit-superforms/client';
2
- import type { Snippet } from 'svelte';
3
2
  import type { Field } from '../../../types/fields.js';
4
3
  type Props = {
5
4
  form: SuperForm<Record<string, unknown>>;
6
5
  fields: Field[];
7
6
  action?: string | undefined;
8
- children?: Snippet;
9
- after?: Snippet;
10
7
  class?: string;
11
8
  };
12
9
  declare const FieldsForm: import("svelte").Component<Props, {}, "">;
@@ -13,14 +13,16 @@
13
13
  import { joinPath } from '../../utils/objectPath.js';
14
14
  import type { ObjectField, ObjectFieldData } from '../../../types/fields.js';
15
15
  import { onMount } from 'svelte';
16
+ import * as Item from '../../../components/ui/item/index.js';
16
17
 
17
18
  type Props = {
19
+ objectFieldType?: 'default' | 'inline';
18
20
  field: ObjectField;
19
21
  form: SuperForm<T>;
20
22
  path: FormPathLeaves<T, ObjectFieldData | undefined>;
21
23
  };
22
24
 
23
- let { field, form, path, ...props }: Props = $props();
25
+ let { field, form, path, objectFieldType = 'default', ...props }: Props = $props();
24
26
 
25
27
  const { value } = formFieldProxy(form, path) satisfies FormFieldProxy<
26
28
  ObjectFieldData | undefined
@@ -49,12 +51,27 @@
49
51
  });
50
52
  </script>
51
53
 
52
- <div class="space-y-8">
53
- {#each field.fields as f}
54
- <FieldRenderer
55
- field={f}
56
- form={form as SuperForm<Record<string, unknown>>}
57
- path={joinPath(path, 'data', f.slug)}
58
- />
59
- {/each}
60
- </div>
54
+ {#snippet content()}
55
+ <div class="space-y-4">
56
+ {#each field.fields as f}
57
+ <FieldRenderer
58
+ field={f}
59
+ form={form as SuperForm<Record<string, unknown>>}
60
+ path={joinPath(path, 'data', f.slug)}
61
+ />
62
+ {/each}
63
+ </div>
64
+ {/snippet}
65
+
66
+ {#if objectFieldType === 'inline'}
67
+ {@render content()}
68
+ {/if}
69
+
70
+ {#if objectFieldType === 'default'}
71
+ <Item.Root variant="outline">
72
+ <Item.Content>
73
+ <Item.Title class="mb-4 text-lg">{field.label}</Item.Title>
74
+ {@render content()}
75
+ </Item.Content>
76
+ </Item.Root>
77
+ {/if}