includio-cms 0.6.0 → 0.6.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/ROADMAP.md +20 -4
  3. package/dist/admin/client/collection/collection-entries.svelte +43 -1
  4. package/dist/admin/client/collection/table-toolbar.svelte +64 -1
  5. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +11 -0
  6. package/dist/admin/components/fields/field-renderer.svelte +3 -2
  7. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -0
  8. package/dist/admin/components/fields/object-field.svelte +5 -5
  9. package/dist/admin/components/fields/object-field.svelte.d.ts +1 -1
  10. package/dist/admin/components/fields/text-field-wrapper.svelte +5 -3
  11. package/dist/admin/components/layout/layout-renderer.svelte +81 -107
  12. package/dist/admin/components/layout/layout-renderer.svelte.d.ts +1 -0
  13. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +13 -6
  14. package/dist/admin/components/tiptap/content-editor.svelte +11 -2
  15. package/dist/admin/styles/admin.css +2 -1
  16. package/dist/ai-claude/index.js +10 -4
  17. package/dist/cli/index.js +10 -3
  18. package/dist/cli/install-peers.d.ts +3 -0
  19. package/dist/cli/install-peers.js +52 -0
  20. package/dist/core/fields/fieldSchemaToTs.js +2 -0
  21. package/dist/core/fields/layoutUtils.d.ts +30 -3
  22. package/dist/core/fields/layoutUtils.js +145 -17
  23. package/dist/core/server/generator/generator.js +21 -10
  24. package/dist/entity/index.d.ts +26 -0
  25. package/dist/entity/index.js +113 -0
  26. package/dist/paraglide/messages/_index.d.ts +36 -3
  27. package/dist/paraglide/messages/_index.js +71 -3
  28. package/dist/paraglide/messages/en.d.ts +5 -0
  29. package/dist/paraglide/messages/en.js +14 -0
  30. package/dist/paraglide/messages/pl.d.ts +5 -0
  31. package/dist/paraglide/messages/pl.js +14 -0
  32. package/dist/types/layout.d.ts +8 -0
  33. package/dist/updates/0.6.0/index.d.ts +2 -0
  34. package/dist/updates/0.6.0/index.js +20 -0
  35. package/dist/updates/index.js +2 -1
  36. package/package.json +20 -6
  37. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  38. package/dist/paraglide/messages/hello_world.js +0 -33
  39. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  40. package/dist/paraglide/messages/login_hello.js +0 -34
  41. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  42. package/dist/paraglide/messages/login_please_login.js +0 -34
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.6.0 — 2026-03-10
7
+
8
+ Entity module, collection filters, layout dot-notation, peerDeps migration
9
+
10
+ ### Added
11
+ - Entity module — programmatic CRUD API for entries (`getEntity`)
12
+ - CLI `install-peers` command for automatic peer dependency installation
13
+ - Collection data filters — filter entries by select/radio field values in toolbar
14
+ - Layout dot-notation — distribute object fields across layout nodes
15
+
16
+ ### Fixed
17
+ - AI Claude: lazy client init, no crash without API key
18
+ - Codegen: quote hyphenated slugs, PascalCase form schemas, improved query types
19
+ - TipTap: inline block content field lang fix + placeholder UX
20
+ - Zod schema: skip lang wrapper for non-localized content field
21
+
22
+ ### Breaking
23
+ - Runtime dependencies moved to peerDependencies — run `pnpm includio install-peers` after upgrade
24
+
6
25
  ## 0.5.8 — 2026-03-09
7
26
 
8
27
  Media GC: deduplicate image styles, auto-cleanup, admin maintenance page
package/ROADMAP.md CHANGED
@@ -122,11 +122,24 @@
122
122
  - [x] `[fix]` `[P1]` resolveUrlFields: normalize flat string url/text, use raw DB calls <!-- files: src/lib/core/server/entries/ -->
123
123
  - [x] `[chore]` `[P1]` Migrate #await to reactive query pattern across admin UI
124
124
 
125
- ## 0.6.0Plugin system _(deferred from 0.2.0)_
125
+ ## 0.5.8Media GC
126
126
 
127
- - [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
128
- - [ ] `[feature]` `[P0]` Plugin registration API public surface for external plugins
129
- - [ ] `[chore]` `[P1]` Plugin system documentation
127
+ - [x] `[feature]` `[P1]` Image style upsert: replace duplicates instead of creating new files
128
+ - [x] `[feature]` `[P1]` Auto-cleanup old style files from disk on regeneration
129
+ - [x] `[feature]` `[P1]` Admin maintenance page: purge styles, reconcile orphaned disk files
130
+ - [x] `[fix]` `[P1]` Deduplicate existing image style records (keep newest per unique key)
131
+
132
+ ## 0.6.0 — Entity module & DX
133
+
134
+ - [x] `[feature]` `[P0]` Entity module — programmatic CRUD API for entries (`getEntity`) <!-- files: src/lib/entity/ -->
135
+ - [x] `[feature]` `[P1]` CLI `install-peers` command <!-- files: src/lib/cli/ -->
136
+ - [x] `[feature]` `[P1]` Collection data filters — filter by select/radio fields in toolbar <!-- files: src/lib/admin/client/collection/ -->
137
+ - [x] `[feature]` `[P1]` Layout dot-notation — distribute object fields across layout nodes <!-- files: src/lib/admin/components/fields/ -->
138
+ - [x] `[fix]` `[P1]` AI Claude: lazy client init, no crash without API key <!-- files: src/lib/ai-claude/ -->
139
+ - [x] `[fix]` `[P1]` Codegen: quote hyphenated slugs, PascalCase form schemas, improved query types <!-- files: src/lib/core/server/generator/ -->
140
+ - [x] `[fix]` `[P1]` TipTap: inline block content field lang fix + placeholder UX
141
+ - [x] `[fix]` `[P1]` Zod schema: skip lang wrapper for non-localized content field
142
+ - [x] `[breaking]` `[P0]` Runtime deps → peerDependencies (run `pnpm includio install-peers`)
130
143
 
131
144
  ## 0.6.1 — Admin experience
132
145
 
@@ -164,3 +177,6 @@
164
177
  - [ ] `[feature]` `[P1]` Impersonation UI — impersonate/stop bar <!-- files: src/lib/admin/client/users/impersonation-bar.svelte -->
165
178
  - [ ] `[chore]` `[P2]` Caching/performance layer (scope TBD)
166
179
  - [ ] `[feature]` `[P2]` API/CLI for configuration (setup DX for less technical users)
180
+ - [ ] `[feature]` `[P0]` Plugin hooks in CRUD ops (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
181
+ - [ ] `[feature]` `[P0]` Plugin registration API — public surface for external plugins
182
+ - [ ] `[chore]` `[P1]` Plugin system documentation
@@ -28,7 +28,7 @@
28
28
  import EmptyState from './empty-state.svelte';
29
29
  import { createCollectionViewState } from './collection-view.svelte.js';
30
30
  import type { EntryStatus as EntryStatusType, RawEntry } from '../../../types/entries.js';
31
- import type { RelationField } from '../../../types/fields.js';
31
+ import type { RelationField, SelectField, RadioField } from '../../../types/fields.js';
32
32
  import {
33
33
  validateA11y,
34
34
  a11yLangPl,
@@ -144,6 +144,31 @@
144
144
  );
145
145
 
146
146
 
147
+ // Data filter configs from select/radio fields in listColumns
148
+ const dataFilterConfigs = $derived(
149
+ (collection.listColumns ?? [])
150
+ .map((slug) => {
151
+ const field = collection.fields.find((f) => f.slug === slug);
152
+ if (field?.type === 'select' || field?.type === 'radio') {
153
+ const f = field as SelectField | RadioField;
154
+ return {
155
+ slug: f.slug,
156
+ label: getLocalizedLabel(f.label, interfaceLanguage.current),
157
+ options: f.options.map((o) => ({
158
+ label: getLocalizedLabel(o.label, interfaceLanguage.current),
159
+ value: o.value
160
+ }))
161
+ };
162
+ }
163
+ return null;
164
+ })
165
+ .filter((f): f is NonNullable<typeof f> => f !== null)
166
+ );
167
+
168
+ let activeDataFilters = $state<Record<string, string | null>>({});
169
+
170
+ const hasActiveDataFilter = $derived(Object.values(activeDataFilters).some((v) => v !== null));
171
+
147
172
  // Find seo field for slug extraction
148
173
  const seoField = $derived(collection.fields.find((f) => f.type === 'seo'));
149
174
 
@@ -588,6 +613,17 @@
588
613
  if (!isArchivedFilter) {
589
614
  rows = filterByStatus(rows);
590
615
  }
616
+ // Client-side data filter (select/radio fields)
617
+ if (hasActiveDataFilter) {
618
+ rows = rows.filter((row) => {
619
+ for (const [slug, value] of Object.entries(activeDataFilters)) {
620
+ if (value === null) continue;
621
+ const rowValue = row.customData[slug];
622
+ if (String(rowValue ?? '') !== value) return false;
623
+ }
624
+ return true;
625
+ });
626
+ }
591
627
  // Client-side search
592
628
  if (searchQuery) {
593
629
  rows = rows.filter((item) => item.searchText.includes(searchQuery.toLowerCase()));
@@ -623,6 +659,12 @@
623
659
  onViewModeChange={(m) => (viewState.viewMode = m)}
624
660
  {onCreateEntry}
625
661
  createLabel={addLabel}
662
+ dataFilters={dataFilterConfigs}
663
+ {activeDataFilters}
664
+ onDataFilterChange={(slug, value) => {
665
+ activeDataFilters = { ...activeDataFilters, [slug]: value };
666
+ viewState.pageIndex = 0;
667
+ }}
626
668
  />
627
669
 
628
670
  {#if totalItems === 0 && !searchQuery}
@@ -11,6 +11,12 @@
11
11
  import type { InterfaceLanguage } from '../../../types/languages.js';
12
12
  import type { ViewMode, StatusFilter } from './collection-view.svelte.js';
13
13
 
14
+ interface DataFilterConfig {
15
+ slug: string;
16
+ label: string;
17
+ options: { label: string; value: string }[];
18
+ }
19
+
14
20
  type Props = {
15
21
  searchQuery: string;
16
22
  onSearchChange: (query: string) => void;
@@ -21,6 +27,9 @@
21
27
  onCreateEntry: () => void;
22
28
  createLabel: string;
23
29
  searchPlaceholder?: string;
30
+ dataFilters?: DataFilterConfig[];
31
+ activeDataFilters?: Record<string, string | null>;
32
+ onDataFilterChange?: (slug: string, value: string | null) => void;
24
33
  };
25
34
 
26
35
  let {
@@ -32,7 +41,10 @@
32
41
  onViewModeChange,
33
42
  onCreateEntry,
34
43
  createLabel,
35
- searchPlaceholder
44
+ searchPlaceholder,
45
+ dataFilters = [],
46
+ activeDataFilters = {},
47
+ onDataFilterChange
36
48
  }: Props = $props();
37
49
 
38
50
  const interfaceLanguage = useInterfaceLanguage();
@@ -92,6 +104,7 @@
92
104
  const hasActiveFilter = $derived(statusFilter !== null);
93
105
 
94
106
  let statusPopoverOpen = $state(false);
107
+ let dataFilterPopovers = $state<Record<string, boolean>>({});
95
108
  </script>
96
109
 
97
110
  <div class="mb-6 flex flex-wrap items-center gap-2.5">
@@ -142,6 +155,56 @@
142
155
  </Popover.Content>
143
156
  </Popover.Root>
144
157
 
158
+ {#each dataFilters as filter}
159
+ {@const activeValue = activeDataFilters[filter.slug] ?? null}
160
+ {@const hasActive = activeValue !== null}
161
+ {@const activeLabel = filter.options.find((o) => o.value === activeValue)?.label ?? ''}
162
+ <Popover.Root open={dataFilterPopovers[filter.slug] ?? false} onOpenChange={(v) => dataFilterPopovers[filter.slug] = v}>
163
+ <Popover.Trigger>
164
+ {#snippet child({ props })}
165
+ <Button
166
+ {...props}
167
+ variant="outline"
168
+ size="sm"
169
+ class="gap-1.5 {hasActive
170
+ ? 'border-primary/30 bg-lavender-lighter text-primary'
171
+ : ''}"
172
+ >
173
+ <Filter class="size-3.5" />
174
+ {filter.label}{hasActive ? `: ${activeLabel}` : ''}
175
+ </Button>
176
+ {/snippet}
177
+ </Popover.Trigger>
178
+ <Popover.Content class="w-44 p-1" align="start">
179
+ <button
180
+ class="hover:bg-accent flex w-full items-center rounded-md px-2.5 py-1.5 text-sm transition-colors {!hasActive
181
+ ? 'bg-accent text-accent-foreground font-medium'
182
+ : 'text-foreground'}"
183
+ onclick={() => {
184
+ onDataFilterChange?.(filter.slug, null);
185
+ dataFilterPopovers[filter.slug] = false;
186
+ }}
187
+ >
188
+ {t.all}
189
+ </button>
190
+ {#each filter.options as option}
191
+ <button
192
+ class="hover:bg-accent flex w-full items-center rounded-md px-2.5 py-1.5 text-sm transition-colors {activeValue ===
193
+ option.value
194
+ ? 'bg-accent text-accent-foreground font-medium'
195
+ : 'text-foreground'}"
196
+ onclick={() => {
197
+ onDataFilterChange?.(filter.slug, option.value);
198
+ dataFilterPopovers[filter.slug] = false;
199
+ }}
200
+ >
201
+ {option.label}
202
+ </button>
203
+ {/each}
204
+ </Popover.Content>
205
+ </Popover.Root>
206
+ {/each}
207
+
145
208
  <div class="flex items-center gap-0.5 rounded-lg border p-0.5">
146
209
  <Button
147
210
  variant={viewMode === 'list' ? 'secondary' : 'ghost'}
@@ -1,4 +1,12 @@
1
1
  import type { ViewMode, StatusFilter } from './collection-view.svelte.js';
2
+ interface DataFilterConfig {
3
+ slug: string;
4
+ label: string;
5
+ options: {
6
+ label: string;
7
+ value: string;
8
+ }[];
9
+ }
2
10
  type Props = {
3
11
  searchQuery: string;
4
12
  onSearchChange: (query: string) => void;
@@ -9,6 +17,9 @@ type Props = {
9
17
  onCreateEntry: () => void;
10
18
  createLabel: string;
11
19
  searchPlaceholder?: string;
20
+ dataFilters?: DataFilterConfig[];
21
+ activeDataFilters?: Record<string, string | null>;
22
+ onDataFilterChange?: (slug: string, value: string | null) => void;
12
23
  };
13
24
  declare const TableToolbar: import("svelte").Component<Props, {}, "">;
14
25
  type TableToolbar = ReturnType<typeof TableToolbar>;
@@ -25,9 +25,10 @@
25
25
  focusedPath?: string | null;
26
26
  flashingPath?: string | null;
27
27
  depth?: number;
28
+ distributed?: boolean;
28
29
  };
29
30
 
30
- let { field, form, path, objectFieldType = 'default', focusedPath = null, flashingPath = null, depth = 0, ...props }: Props = $props();
31
+ let { field, form, path, objectFieldType = 'default', focusedPath = null, flashingPath = null, depth = 0, distributed = false, ...props }: Props = $props();
31
32
 
32
33
  const interfaceLanguage = useInterfaceLanguage();
33
34
 
@@ -137,7 +138,7 @@
137
138
  <ArrayField {field} bind:value={$value} />
138
139
  {/await}
139
140
  {:else if field.type === 'object'}
140
- <LazyField loader={() => import('./object-field.svelte')} props={{ field, form, path, objectFieldType, focusedPath, flashingPath, depth }} skeletonClass="h-20" />
141
+ <LazyField loader={() => import('./object-field.svelte')} props={{ field, form, path, objectFieldType: distributed ? 'distributed' : objectFieldType, focusedPath, flashingPath, depth }} skeletonClass="h-20" />
141
142
  {:else if field.type === 'slug'}
142
143
  <LazyField loader={() => import('./slug-field.svelte')} props={{ field, form, path }} skeletonClass="h-10" />
143
144
  {:else if field.type === 'boolean'}
@@ -8,6 +8,7 @@ type Props = {
8
8
  focusedPath?: string | null;
9
9
  flashingPath?: string | null;
10
10
  depth?: number;
11
+ distributed?: boolean;
11
12
  };
12
13
  declare const FieldRenderer: import("svelte").Component<Props, {}, "">;
13
14
  type FieldRenderer = ReturnType<typeof FieldRenderer>;
@@ -15,7 +15,7 @@
15
15
  import { cn } from '../../../utils.js';
16
16
 
17
17
  type Props = {
18
- objectFieldType?: 'default' | 'inline';
18
+ objectFieldType?: 'default' | 'inline' | 'distributed';
19
19
  field: ObjectField;
20
20
  form: SuperForm<Record<string, unknown>>;
21
21
  path: FormPathLeaves<Record<string, unknown>>;
@@ -83,11 +83,11 @@
83
83
  </div>
84
84
  {/snippet}
85
85
 
86
- {#if objectFieldType === 'inline'}
86
+ {#if objectFieldType === 'distributed'}
87
+ <!-- Distributed mode: layout handles field rendering, object only initializes data -->
88
+ {:else if objectFieldType === 'inline'}
87
89
  {@render content()}
88
- {/if}
89
-
90
- {#if objectFieldType === 'default'}
90
+ {:else if objectFieldType === 'default'}
91
91
  {#if depth >= 1}
92
92
  <!-- Nested: lighter style without border -->
93
93
  <div class="space-y-3 pt-1">
@@ -1,7 +1,7 @@
1
1
  import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
2
2
  import type { ObjectField } from '../../../types/fields.js';
3
3
  type Props = {
4
- objectFieldType?: 'default' | 'inline';
4
+ objectFieldType?: 'default' | 'inline' | 'distributed';
5
5
  field: ObjectField;
6
6
  form: SuperForm<Record<string, unknown>>;
7
7
  path: FormPathLeaves<Record<string, unknown>>;
@@ -15,6 +15,7 @@
15
15
  import { joinPath, setAtPath } from '../../utils/objectPath.js';
16
16
  import { getContentLanguage } from '../../state/content-language.svelte.js';
17
17
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
18
+ import { getContext } from 'svelte';
18
19
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
19
20
  import RequiredLabel from './required-label.svelte';
20
21
  import ClipboardIcon from '@tabler/icons-svelte/icons/clipboard';
@@ -23,6 +24,7 @@
23
24
 
24
25
  const contentLanguage = getContentLanguage();
25
26
  const interfaceLanguage = useInterfaceLanguage();
27
+ const inInlineBlock = getContext<boolean>('inInlineBlock') ?? false;
26
28
 
27
29
  type Props = {
28
30
  field: TextFieldType | RichtextFieldType | ContentFieldType;
@@ -160,7 +162,7 @@
160
162
  {#if field.label}
161
163
  <div class="flex items-center gap-1.5">
162
164
  <RequiredLabel required={field.required}>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel>
163
- {#if isMultiLang && (field.required || contentLanguage.all.some(l => getLangFillStatus(l)))}
165
+ {#if isMultiLang && !inInlineBlock && (field.required || contentLanguage.all.some(l => getLangFillStatus(l)))}
164
166
  <span
165
167
  class="inline-flex items-center gap-1"
166
168
  title={dotTooltip()}
@@ -176,7 +178,7 @@
176
178
  </div>
177
179
  {/if}
178
180
 
179
- {#if contentLanguage.referenceMode && isMultiLang}
181
+ {#if contentLanguage.referenceMode && isMultiLang && !inInlineBlock}
180
182
  {@const refSource = findSourceLang(lang)}
181
183
  {#if refSource}
182
184
  {@const refText = getReferenceText(refSource)}
@@ -199,7 +201,7 @@
199
201
  <div class="h-32 animate-pulse rounded-md bg-accent"></div>
200
202
  {/if}
201
203
 
202
- {#if isMultiLang}
204
+ {#if isMultiLang && !inInlineBlock}
203
205
  {@const currentVal = resolvePathValue($formData, joinPath(path, lang))}
204
206
  {@const sourceLang = isValueEmpty(currentVal) ? findSourceLang(lang) : null}
205
207
  {#if sourceLang}
@@ -9,6 +9,11 @@
9
9
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
10
10
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
11
11
  import { cn } from '../../../utils.js';
12
+ import {
13
+ resolveFieldByPath,
14
+ buildFormPath,
15
+ getDistributedObjectSlugs
16
+ } from '../../../core/fields/layoutUtils.js';
12
17
 
13
18
  type Props = {
14
19
  nodes: LayoutNode[];
@@ -17,6 +22,7 @@
17
22
  focusedPath?: string | null;
18
23
  flashingPath?: string | null;
19
24
  depth?: number;
25
+ distributedSlugs?: Set<string>;
20
26
  };
21
27
 
22
28
  let {
@@ -25,7 +31,8 @@
25
31
  form,
26
32
  focusedPath = null,
27
33
  flashingPath = null,
28
- depth = 0
34
+ depth = 0,
35
+ distributedSlugs: parentDistributedSlugs
29
36
  }: Props = $props();
30
37
 
31
38
  const interfaceLanguage = useInterfaceLanguage();
@@ -33,6 +40,26 @@
33
40
 
34
41
  const fieldMap = $derived(new Map(fields.map((f) => [f.slug, f])));
35
42
 
43
+ // Compute distributed object slugs once at top level
44
+ const distributedSlugs = $derived(parentDistributedSlugs ?? getDistributedObjectSlugs(nodes, fields));
45
+
46
+ /**
47
+ * Resolve a field reference (slug or dot-notation path) to its Field definition and form path.
48
+ * Returns { field, formPath } or undefined if not found.
49
+ */
50
+ function resolveFieldRef(ref: string): { field: Field; formPath: string } | undefined {
51
+ if (!ref.includes('.')) {
52
+ // Simple slug — top-level field
53
+ const field = fieldMap.get(ref);
54
+ if (!field) return undefined;
55
+ return { field, formPath: ref };
56
+ }
57
+ // Dot-notation — resolve through field tree
58
+ const field = resolveFieldByPath(fields, ref);
59
+ if (!field) return undefined;
60
+ return { field, formPath: buildFormPath(ref) };
61
+ }
62
+
36
63
  // Compact field types for autoGrid
37
64
  const compactFieldTypes = new Set(['number', 'select', 'boolean', 'date', 'datetime', 'radio']);
38
65
 
@@ -52,6 +79,44 @@
52
79
  }
53
80
  </script>
54
81
 
82
+ {#snippet fieldSlot(ref: string, autoGrid?: boolean)}
83
+ {@const resolved = resolveFieldRef(ref)}
84
+ {#if resolved}
85
+ {@const { field, formPath } = resolved}
86
+ {#if evaluateCondition(field.showWhen, (s) => $formData[s])}
87
+ <div
88
+ data-field-path={formPath}
89
+ class={cn(
90
+ 'rounded-lg transition-all duration-500',
91
+ autoGrid && !isCompactField(field) && 'auto-grid-full',
92
+ isFlashing(formPath) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
93
+ )}
94
+ >
95
+ <FieldRenderer
96
+ {field}
97
+ {form}
98
+ path={formPath}
99
+ {focusedPath}
100
+ {flashingPath}
101
+ distributed={field.type === 'object' && distributedSlugs.has(field.slug)}
102
+ />
103
+ </div>
104
+ {/if}
105
+ {/if}
106
+ {/snippet}
107
+
108
+ {#snippet recurse(childNodes: LayoutNode[])}
109
+ <svelte:self
110
+ nodes={childNodes}
111
+ {fields}
112
+ {form}
113
+ {focusedPath}
114
+ {flashingPath}
115
+ depth={depth + 1}
116
+ {distributedSlugs}
117
+ />
118
+ {/snippet}
119
+
55
120
  {#each nodes as node (node)}
56
121
  {#if node.type === 'section'}
57
122
  <section aria-label={getLabel(node)} class="layout-section">
@@ -60,31 +125,13 @@
60
125
  </div>
61
126
  {#if isLayoutLeaf(node)}
62
127
  <div class="layout-fields-stack">
63
- {#each node.fields as slug (slug)}
64
- {@const field = fieldMap.get(slug)}
65
- {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
66
- <div
67
- data-field-path={slug}
68
- class={cn(
69
- 'rounded-lg transition-all duration-500',
70
- isFlashing(slug) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
71
- )}
72
- >
73
- <FieldRenderer {field} {form} path={slug} {focusedPath} {flashingPath} />
74
- </div>
75
- {/if}
128
+ {#each node.fields as ref (ref)}
129
+ {@render fieldSlot(ref)}
76
130
  {/each}
77
131
  </div>
78
132
  {/if}
79
133
  {#if isLayoutBranch(node)}
80
- <svelte:self
81
- nodes={node.children}
82
- {fields}
83
- {form}
84
- {focusedPath}
85
- {flashingPath}
86
- depth={depth + 1}
87
- />
134
+ {@render recurse(node.children)}
88
135
  {/if}
89
136
  </section>
90
137
 
@@ -96,14 +143,7 @@
96
143
  {#if isLayoutBranch(node)}
97
144
  {#each node.children as child (child)}
98
145
  <div class="layout-column">
99
- <svelte:self
100
- nodes={[child]}
101
- {fields}
102
- {form}
103
- {focusedPath}
104
- {flashingPath}
105
- depth={depth + 1}
106
- />
146
+ {@render recurse([child])}
107
147
  </div>
108
148
  {/each}
109
149
  {/if}
@@ -117,49 +157,19 @@
117
157
  <div class="layout-card-body">
118
158
  {#if isLayoutLeaf(node) && node.autoGrid}
119
159
  <div class="layout-auto-grid">
120
- {#each node.fields as slug (slug)}
121
- {@const field = fieldMap.get(slug)}
122
- {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
123
- <div
124
- data-field-path={slug}
125
- class={cn(
126
- 'rounded-lg transition-all duration-500',
127
- !isCompactField(field) && 'auto-grid-full',
128
- isFlashing(slug) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
129
- )}
130
- >
131
- <FieldRenderer {field} {form} path={slug} {focusedPath} {flashingPath} />
132
- </div>
133
- {/if}
160
+ {#each node.fields as ref (ref)}
161
+ {@render fieldSlot(ref, true)}
134
162
  {/each}
135
163
  </div>
136
164
  {:else if isLayoutLeaf(node)}
137
165
  <div class="layout-fields-stack">
138
- {#each node.fields as slug (slug)}
139
- {@const field = fieldMap.get(slug)}
140
- {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
141
- <div
142
- data-field-path={slug}
143
- class={cn(
144
- 'rounded-lg transition-all duration-500',
145
- isFlashing(slug) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
146
- )}
147
- >
148
- <FieldRenderer {field} {form} path={slug} {focusedPath} {flashingPath} />
149
- </div>
150
- {/if}
166
+ {#each node.fields as ref (ref)}
167
+ {@render fieldSlot(ref)}
151
168
  {/each}
152
169
  </div>
153
170
  {/if}
154
171
  {#if isLayoutBranch(node)}
155
- <svelte:self
156
- nodes={node.children}
157
- {fields}
158
- {form}
159
- {focusedPath}
160
- {flashingPath}
161
- depth={depth + 1}
162
- />
172
+ {@render recurse(node.children)}
163
173
  {/if}
164
174
  </div>
165
175
  </div>
@@ -174,31 +184,13 @@
174
184
  <Accordion.Content>
175
185
  {#if isLayoutLeaf(node)}
176
186
  <div class="layout-fields-stack">
177
- {#each node.fields as slug (slug)}
178
- {@const field = fieldMap.get(slug)}
179
- {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
180
- <div
181
- data-field-path={slug}
182
- class={cn(
183
- 'rounded-lg transition-all duration-500',
184
- isFlashing(slug) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
185
- )}
186
- >
187
- <FieldRenderer {field} {form} path={slug} {focusedPath} {flashingPath} />
188
- </div>
189
- {/if}
187
+ {#each node.fields as ref (ref)}
188
+ {@render fieldSlot(ref)}
190
189
  {/each}
191
190
  </div>
192
191
  {/if}
193
192
  {#if isLayoutBranch(node)}
194
- <svelte:self
195
- nodes={node.children}
196
- {fields}
197
- {form}
198
- {focusedPath}
199
- {flashingPath}
200
- depth={depth + 1}
201
- />
193
+ {@render recurse(node.children)}
202
194
  {/if}
203
195
  </Accordion.Content>
204
196
  </Accordion.Item>
@@ -208,30 +200,12 @@
208
200
  {:else if node.type === 'stack'}
209
201
  <div class="layout-stack">
210
202
  {#if isLayoutLeaf(node)}
211
- {#each node.fields as slug (slug)}
212
- {@const field = fieldMap.get(slug)}
213
- {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
214
- <div
215
- data-field-path={slug}
216
- class={cn(
217
- 'rounded-lg transition-all duration-500',
218
- isFlashing(slug) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
219
- )}
220
- >
221
- <FieldRenderer {field} {form} path={slug} {focusedPath} {flashingPath} />
222
- </div>
223
- {/if}
203
+ {#each node.fields as ref (ref)}
204
+ {@render fieldSlot(ref)}
224
205
  {/each}
225
206
  {/if}
226
207
  {#if isLayoutBranch(node)}
227
- <svelte:self
228
- nodes={node.children}
229
- {fields}
230
- {form}
231
- {focusedPath}
232
- {flashingPath}
233
- depth={depth + 1}
234
- />
208
+ {@render recurse(node.children)}
235
209
  {/if}
236
210
  </div>
237
211
  {/if}
@@ -8,6 +8,7 @@ type Props = {
8
8
  focusedPath?: string | null;
9
9
  flashingPath?: string | null;
10
10
  depth?: number;
11
+ distributedSlugs?: Set<string>;
11
12
  };
12
13
  declare const LayoutRenderer: import("svelte").Component<Props, {}, "">;
13
14
  type LayoutRenderer = ReturnType<typeof LayoutRenderer>;