@lobb-js/studio 0.48.0 → 0.49.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 (63) hide show
  1. package/dist/components/detailView/detailView.svelte +12 -62
  2. package/dist/components/detailView/detailView.svelte.d.ts +1 -1
  3. package/dist/components/detailView/fieldInput.svelte +161 -288
  4. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -1
  5. package/dist/components/detailView/fields/BoolField.svelte +42 -0
  6. package/dist/components/detailView/fields/BoolField.svelte.d.ts +13 -0
  7. package/dist/components/detailView/fields/CodeField.svelte +30 -0
  8. package/dist/components/detailView/fields/CodeField.svelte.d.ts +13 -0
  9. package/dist/components/detailView/fields/CustomInputField.svelte +50 -0
  10. package/dist/components/detailView/fields/CustomInputField.svelte.d.ts +18 -0
  11. package/dist/components/detailView/fields/DateField.svelte +47 -0
  12. package/dist/components/detailView/fields/DateField.svelte.d.ts +14 -0
  13. package/dist/components/detailView/fields/EmbeddedField.svelte +139 -0
  14. package/dist/components/detailView/fields/EmbeddedField.svelte.d.ts +13 -0
  15. package/dist/components/detailView/fields/EmbeddedPolymorphicField.svelte +197 -0
  16. package/dist/components/detailView/fields/EmbeddedPolymorphicField.svelte.d.ts +13 -0
  17. package/dist/components/detailView/fields/EnumField.svelte +70 -0
  18. package/dist/components/detailView/fields/EnumField.svelte.d.ts +17 -0
  19. package/dist/components/detailView/fields/FieldWrapper.svelte +68 -0
  20. package/dist/components/detailView/fields/FieldWrapper.svelte.d.ts +18 -0
  21. package/dist/components/detailView/fields/ForeignKeyField.svelte +78 -0
  22. package/dist/components/detailView/fields/ForeignKeyField.svelte.d.ts +17 -0
  23. package/dist/components/detailView/fields/IdField.svelte +21 -0
  24. package/dist/components/detailView/fields/IdField.svelte.d.ts +12 -0
  25. package/dist/components/detailView/fields/NumberField.svelte +38 -0
  26. package/dist/components/detailView/fields/NumberField.svelte.d.ts +16 -0
  27. package/dist/components/detailView/fields/PasswordField.svelte +29 -0
  28. package/dist/components/detailView/fields/PasswordField.svelte.d.ts +12 -0
  29. package/dist/components/detailView/fields/PolymorphicField.svelte +51 -0
  30. package/dist/components/detailView/fields/PolymorphicField.svelte.d.ts +16 -0
  31. package/dist/components/detailView/fields/RichTextField.svelte +30 -0
  32. package/dist/components/detailView/fields/RichTextField.svelte.d.ts +13 -0
  33. package/dist/components/detailView/fields/StringField.svelte +35 -0
  34. package/dist/components/detailView/fields/StringField.svelte.d.ts +14 -0
  35. package/dist/components/detailView/fields/TextField.svelte +35 -0
  36. package/dist/components/detailView/fields/TextField.svelte.d.ts +14 -0
  37. package/dist/components/foreingKeyInput.svelte +1 -1
  38. package/dist/components/polymorphicInput.svelte +1 -1
  39. package/dist/extensions/extension.types.d.ts +3 -1
  40. package/dist/extensions/extensionUtils.js +2 -0
  41. package/package.json +7 -6
  42. package/src/lib/components/detailView/detailView.svelte +12 -62
  43. package/src/lib/components/detailView/fieldInput.svelte +161 -288
  44. package/src/lib/components/detailView/fields/BoolField.svelte +42 -0
  45. package/src/lib/components/detailView/fields/CodeField.svelte +30 -0
  46. package/src/lib/components/detailView/fields/CustomInputField.svelte +50 -0
  47. package/src/lib/components/detailView/fields/DateField.svelte +47 -0
  48. package/src/lib/components/detailView/fields/EmbeddedField.svelte +139 -0
  49. package/src/lib/components/detailView/fields/EmbeddedPolymorphicField.svelte +197 -0
  50. package/src/lib/components/detailView/fields/EnumField.svelte +70 -0
  51. package/src/lib/components/detailView/fields/FieldWrapper.svelte +68 -0
  52. package/src/lib/components/detailView/fields/ForeignKeyField.svelte +78 -0
  53. package/src/lib/components/detailView/fields/IdField.svelte +21 -0
  54. package/src/lib/components/detailView/fields/NumberField.svelte +38 -0
  55. package/src/lib/components/detailView/fields/PasswordField.svelte +29 -0
  56. package/src/lib/components/detailView/fields/PolymorphicField.svelte +51 -0
  57. package/src/lib/components/detailView/fields/RichTextField.svelte +30 -0
  58. package/src/lib/components/detailView/fields/StringField.svelte +35 -0
  59. package/src/lib/components/detailView/fields/TextField.svelte +35 -0
  60. package/src/lib/components/foreingKeyInput.svelte +1 -1
  61. package/src/lib/components/polymorphicInput.svelte +1 -1
  62. package/src/lib/extensions/extension.types.ts +2 -1
  63. package/src/lib/extensions/extensionUtils.ts +2 -0
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import CodeEditor from "../../codeEditor.svelte";
4
+ import FieldWrapper from "./FieldWrapper.svelte";
5
+
6
+ interface Props {
7
+ value?: any;
8
+ field: any;
9
+ args?: any;
10
+ isDisabled?: boolean;
11
+ errorMessages?: any;
12
+ header?: any;
13
+ clearBtn?: Snippet<[]>;
14
+ }
15
+
16
+ let { value = $bindable(), field, args, isDisabled = false, errorMessages = [], header, clearBtn }: Props = $props();
17
+ </script>
18
+
19
+ <FieldWrapper span={2} {isDisabled} {errorMessages} {header}>
20
+ {#snippet children()}
21
+ <div class="relative">
22
+ {#if clearBtn}
23
+ <div class="absolute right-1 top-1 z-10">
24
+ {@render clearBtn()}
25
+ </div>
26
+ {/if}
27
+ <CodeEditor name={field.key} type={args?.type ?? 'javascript'} bind:value />
28
+ </div>
29
+ {/snippet}
30
+ </FieldWrapper>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ value?: any;
4
+ field: any;
5
+ args?: any;
6
+ isDisabled?: boolean;
7
+ errorMessages?: any;
8
+ header?: any;
9
+ clearBtn?: Snippet<[]>;
10
+ }
11
+ declare const CodeField: import("svelte").Component<Props, {}, "value">;
12
+ type CodeField = ReturnType<typeof CodeField>;
13
+ export default CodeField;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import FieldCustomInput from "../fieldCustomInput.svelte";
4
+ import FieldWrapper from "./FieldWrapper.svelte";
5
+
6
+ interface Props {
7
+ value?: any;
8
+ field: any;
9
+ uiInput: { type: string; args?: any };
10
+ destructive?: boolean;
11
+ changedClass?: string;
12
+ isDisabled?: boolean;
13
+ errorMessages?: any;
14
+ header?: any;
15
+ clearBtn?: Snippet<[]>;
16
+ }
17
+
18
+ let {
19
+ value = $bindable(),
20
+ field,
21
+ uiInput,
22
+ destructive = false,
23
+ changedClass = '',
24
+ isDisabled = false,
25
+ errorMessages = [],
26
+ header,
27
+ clearBtn,
28
+ }: Props = $props();
29
+
30
+ const isFullWidth = uiInput.type === "richtext";
31
+ </script>
32
+
33
+ <FieldWrapper span={isFullWidth ? 2 : 1} {isDisabled} {errorMessages} {header}>
34
+ {#snippet children()}
35
+ <div class="relative {destructive ? 'rounded-md ring-1 ring-destructive bg-destructive/10' : ''}">
36
+ {#if clearBtn}
37
+ <div class="absolute right-1 z-10 {isFullWidth ? 'top-1' : 'inset-y-0 flex items-center'}">
38
+ {@render clearBtn()}
39
+ </div>
40
+ {/if}
41
+ <FieldCustomInput
42
+ bind:value
43
+ type={uiInput.type}
44
+ args={uiInput.args}
45
+ {field}
46
+ {destructive}
47
+ />
48
+ </div>
49
+ {/snippet}
50
+ </FieldWrapper>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ value?: any;
4
+ field: any;
5
+ uiInput: {
6
+ type: string;
7
+ args?: any;
8
+ };
9
+ destructive?: boolean;
10
+ changedClass?: string;
11
+ isDisabled?: boolean;
12
+ errorMessages?: any;
13
+ header?: any;
14
+ clearBtn?: Snippet<[]>;
15
+ }
16
+ declare const CustomInputField: import("svelte").Component<Props, {}, "value">;
17
+ type CustomInputField = ReturnType<typeof CustomInputField>;
18
+ export default CustomInputField;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import Input from "../../ui/input/input.svelte";
4
+ import FieldWrapper from "./FieldWrapper.svelte";
5
+
6
+ interface Props {
7
+ value?: any;
8
+ type: "date" | "time" | "datetime";
9
+ placeholder?: string;
10
+ destructive?: boolean;
11
+ isDisabled?: boolean;
12
+ errorMessages?: any;
13
+ header?: any;
14
+ clearBtn?: Snippet<[]>;
15
+ }
16
+ let { value = $bindable(), type, placeholder = "NULL", destructive = false, isDisabled = false, errorMessages = [], header, clearBtn }: Props = $props();
17
+
18
+ const inputType = type === "datetime" ? "datetime-local" : type;
19
+
20
+ function getDisplayValue() {
21
+ if (!value) return;
22
+ if (type === "date") return new Date(value).toISOString().split("T")[0];
23
+ if (type === "datetime") return new Date(value).toISOString().slice(0, 16);
24
+ return value;
25
+ }
26
+ </script>
27
+
28
+ <FieldWrapper span={1} {isDisabled} {errorMessages} {header}>
29
+ {#snippet children()}
30
+ <div class="relative">
31
+ {#if clearBtn}
32
+ <div class="absolute right-1 inset-y-0 z-10 flex items-center">
33
+ {@render clearBtn()}
34
+ </div>
35
+ {/if}
36
+ <Input
37
+ type={inputType}
38
+ {placeholder}
39
+ class="dateInput block w-full bg-muted pr-9 text-xs {destructive ? 'border-destructive' : ''}"
40
+ bind:value={
41
+ () => getDisplayValue(),
42
+ (v) => { value = v; }
43
+ }
44
+ />
45
+ </div>
46
+ {/snippet}
47
+ </FieldWrapper>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ value?: any;
4
+ type: "date" | "time" | "datetime";
5
+ placeholder?: string;
6
+ destructive?: boolean;
7
+ isDisabled?: boolean;
8
+ errorMessages?: any;
9
+ header?: any;
10
+ clearBtn?: Snippet<[]>;
11
+ }
12
+ declare const DateField: import("svelte").Component<Props, {}, "value">;
13
+ type DateField = ReturnType<typeof DateField>;
14
+ export default DateField;
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack } from "svelte";
3
+ import { Ban, Plus, Table, icons } from "lucide-svelte";
4
+ import Button from "../../ui/button/button.svelte";
5
+ import { getStudioContext } from "../../../context";
6
+ import DetailView from "../detailView.svelte";
7
+ import { getDefaultEntry } from "../utils";
8
+ import { getChangedProperties } from "../../../utils";
9
+ import FieldWrapper from "./FieldWrapper.svelte";
10
+
11
+ const { lobb, ctx } = getStudioContext();
12
+
13
+ interface Props {
14
+ collectionName: string;
15
+ fieldName: string;
16
+ value?: any;
17
+ entry: Record<string, any>;
18
+ isDisabled?: boolean;
19
+ errorMessages?: any;
20
+ header?: any;
21
+ onClear?: () => void;
22
+ }
23
+
24
+ let {
25
+ collectionName,
26
+ fieldName,
27
+ value = $bindable(),
28
+ entry = $bindable(),
29
+ isDisabled = false,
30
+ errorMessages = [],
31
+ header,
32
+ onClear,
33
+ }: Props = $props();
34
+
35
+ const destructive = $derived(Array.isArray(errorMessages) && errorMessages.length > 0);
36
+
37
+ const CollectionIcon = $derived.by(() => {
38
+ const name = ctx.meta.collections[collectionName]?.ui?.icon;
39
+ return name && name in icons ? (icons as any)[name] : Table;
40
+ });
41
+
42
+ const fieldNames = Object.keys(ctx.meta.collections[collectionName]?.fields ?? {});
43
+ let embeddedValues = $state(getDefaultEntry(ctx, fieldNames, collectionName, {}));
44
+ let initialValues = $state<Record<string, any> | null>(null);
45
+ let loaded = $state(false);
46
+ let creating = $state(false);
47
+
48
+ const hasValue = $derived(value != null);
49
+ const hasNestedErrors = $derived(
50
+ errorMessages && typeof errorMessages === 'object' && !Array.isArray(errorMessages) &&
51
+ errorMessages.details && Object.keys(errorMessages.details).length > 0
52
+ );
53
+ const showForm = $derived(hasValue || creating || hasNestedErrors);
54
+
55
+ onMount(async () => {
56
+ const fkId = typeof value === 'number' ? value : null;
57
+ if (fkId != null) {
58
+ try {
59
+ const res = await lobb.findAll(collectionName, { filter: { id: fkId }, limit: 1 });
60
+ const result = await res.json();
61
+ const record = result.data[0] ?? {};
62
+ embeddedValues = getDefaultEntry(ctx, fieldNames, collectionName, record);
63
+ initialValues = { ...embeddedValues };
64
+ } catch { /* leave defaults */ }
65
+ } else if (value && typeof value === 'object' && value.create) {
66
+ embeddedValues = getDefaultEntry(ctx, fieldNames, collectionName, value.create);
67
+ creating = true;
68
+ }
69
+ loaded = true;
70
+ });
71
+
72
+ $effect(() => {
73
+ if (!loaded) return;
74
+ const snap = { ...embeddedValues };
75
+ const isCreating = creating; // track so the effect re-runs when user clicks Create
76
+ untrack(() => {
77
+ const fkId = typeof value === 'number' ? value : (value?.id ?? null);
78
+ if (fkId != null && initialValues) {
79
+ const changed = getChangedProperties(initialValues, snap);
80
+ value = Object.keys(changed).length > 0 ? { id: fkId, update: changed } : fkId;
81
+ } else if (isCreating) {
82
+ const data: Record<string, any> = {};
83
+ for (const [k, v] of Object.entries(snap)) {
84
+ if (v !== null && v !== undefined && v !== '') data[k] = v;
85
+ }
86
+ value = { create: data };
87
+ }
88
+ });
89
+ });
90
+
91
+ function handleRemove() {
92
+ creating = false;
93
+ embeddedValues = getDefaultEntry(ctx, fieldNames, collectionName, {});
94
+ initialValues = null;
95
+ onClear?.();
96
+ }
97
+ </script>
98
+
99
+ <FieldWrapper span={2} {isDisabled} {errorMessages}>
100
+ {#snippet children()}
101
+ <div class="rounded-md border bg-muted/30 flex flex-col {destructive ? 'border-destructive' : ''}">
102
+ {#if showForm}
103
+ <div class="flex items-center justify-between gap-2 px-4 py-2.5 border-b">
104
+ {#if header}
105
+ <div class="flex items-center gap-2">
106
+ <CollectionIcon size={14} class="text-muted-foreground" />
107
+ <span class="text-sm font-medium">{header.text}</span>
108
+ <span class="text-[0.7rem] text-muted-foreground">{collectionName}</span>
109
+ </div>
110
+ {/if}
111
+ {#if !isDisabled}
112
+ <Button onclick={handleRemove} variant="outline" class="aspect-square h-6 w-6 shrink-0 p-0" Icon={Ban} tabindex={-1} />
113
+ {/if}
114
+ </div>
115
+ {#if loaded}
116
+ <DetailView {collectionName} bind:entry={embeddedValues} fieldsErrors={errorMessages?.details ?? {}} />
117
+ {/if}
118
+ {:else}
119
+ <div class="flex flex-col items-center gap-3 py-8 px-4 text-center">
120
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
121
+ <CollectionIcon size={20} class="text-muted-foreground" />
122
+ </div>
123
+ <div class="flex flex-col items-center gap-2">
124
+ <div class="flex items-center gap-2">
125
+ <span class="text-sm font-semibold">No record attached in</span>
126
+ <span class="rounded-md border bg-background px-2.5 py-0.5 text-xs font-semibold tracking-wide">{header?.text ?? collectionName}</span>
127
+ </div>
128
+ <span class="text-xs text-muted-foreground">Create a new record to attach it here.</span>
129
+ </div>
130
+ {#if !isDisabled}
131
+ <Button onclick={() => creating = true} class="mt-1 gap-1.5" Icon={Plus}>
132
+ Create
133
+ </Button>
134
+ {/if}
135
+ </div>
136
+ {/if}
137
+ </div>
138
+ {/snippet}
139
+ </FieldWrapper>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ collectionName: string;
3
+ fieldName: string;
4
+ value?: any;
5
+ entry: Record<string, any>;
6
+ isDisabled?: boolean;
7
+ errorMessages?: any;
8
+ header?: any;
9
+ onClear?: () => void;
10
+ }
11
+ declare const EmbeddedField: import("svelte").Component<Props, {}, "value" | "entry">;
12
+ type EmbeddedField = ReturnType<typeof EmbeddedField>;
13
+ export default EmbeddedField;
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack } from "svelte";
3
+ import { Ban, ChevronDown, GitFork, Plus, Table, icons } from "lucide-svelte";
4
+ import Button from "../../ui/button/button.svelte";
5
+ import * as Popover from "../../ui/popover/index";
6
+ import { getStudioContext } from "../../../context";
7
+ import DetailView from "../detailView.svelte";
8
+ import { getDefaultEntry } from "../utils";
9
+ import { getChangedProperties } from "../../../utils";
10
+ import FieldWrapper from "./FieldWrapper.svelte";
11
+
12
+ const { lobb, ctx } = getStudioContext();
13
+
14
+ interface Props {
15
+ virtualField: string;
16
+ collectionField: string;
17
+ idField: string;
18
+ targetCollections: string[];
19
+ entry: Record<string, any>;
20
+ isDisabled?: boolean;
21
+ errorMessages?: any;
22
+ header?: any;
23
+ }
24
+
25
+ let {
26
+ virtualField,
27
+ collectionField,
28
+ idField,
29
+ targetCollections,
30
+ entry = $bindable(),
31
+ isDisabled = false,
32
+ errorMessages = [],
33
+ header,
34
+ }: Props = $props();
35
+
36
+ const selectedCollection = $derived(
37
+ (entry[virtualField]?.collection ?? entry[collectionField]) as string | null ?? null
38
+ );
39
+
40
+ const CollectionIcon = $derived.by(() => {
41
+ if (!selectedCollection) return GitFork;
42
+ const name = ctx.meta.collections[selectedCollection]?.ui?.icon;
43
+ return name && name in icons ? (icons as any)[name] : Table;
44
+ });
45
+
46
+ const destructive = $derived(Array.isArray(errorMessages) && errorMessages.length > 0);
47
+ const hasNestedErrors = $derived(
48
+ errorMessages && typeof errorMessages === 'object' && !Array.isArray(errorMessages) &&
49
+ errorMessages.details && Object.keys(errorMessages.details).length > 0
50
+ );
51
+
52
+ const showForm = $derived(!!selectedCollection || hasNestedErrors);
53
+
54
+ // --- embedded form state ---
55
+ let embeddedValues = $state<Record<string, any>>({});
56
+ let initialValues = $state<Record<string, any> | null>(null);
57
+ let loaded = $state(false);
58
+
59
+ async function loadEmbedded(col: string) {
60
+ loaded = false;
61
+ const fieldNames = Object.keys(ctx.meta.collections[col]?.fields ?? {});
62
+ const fkId = typeof entry[idField] === 'number' ? entry[idField] : null;
63
+ const virtualVal = entry[virtualField];
64
+
65
+ if (fkId != null) {
66
+ try {
67
+ const res = await lobb.findAll(col, { filter: { id: fkId }, limit: 1 });
68
+ const result = await res.json();
69
+ const record = result.data[0] ?? {};
70
+ embeddedValues = getDefaultEntry(ctx, fieldNames, col, record);
71
+ initialValues = { ...embeddedValues };
72
+ } catch { /* leave defaults */ }
73
+ } else if (virtualVal?.create && typeof virtualVal.create === 'object') {
74
+ embeddedValues = getDefaultEntry(ctx, fieldNames, col, virtualVal.create);
75
+ } else {
76
+ embeddedValues = getDefaultEntry(ctx, fieldNames, col, {});
77
+ }
78
+ loaded = true;
79
+ }
80
+
81
+ onMount(() => {
82
+ if (selectedCollection) loadEmbedded(selectedCollection);
83
+ else loaded = true;
84
+ });
85
+
86
+ $effect(() => {
87
+ if (!loaded || !selectedCollection) return;
88
+ const col = selectedCollection;
89
+ const snap = { ...embeddedValues };
90
+ untrack(() => {
91
+ const fkId = typeof entry[idField] === 'number' ? entry[idField] : null;
92
+ if (fkId != null && initialValues) {
93
+ const changed = getChangedProperties(initialValues, snap);
94
+ entry[virtualField] = Object.keys(changed).length > 0
95
+ ? { collection: col, id: fkId, update: changed }
96
+ : null;
97
+ } else {
98
+ const data: Record<string, any> = {};
99
+ for (const [k, v] of Object.entries(snap)) {
100
+ if (v !== null && v !== undefined && v !== '') data[k] = v;
101
+ }
102
+ entry[virtualField] = { collection: col, create: data };
103
+ }
104
+ });
105
+ });
106
+
107
+ function handleSelect(name: string) {
108
+ entry[virtualField] = { collection: name };
109
+ embeddedValues = {};
110
+ initialValues = null;
111
+ loadEmbedded(name);
112
+ }
113
+
114
+ function handleClear() {
115
+ entry[virtualField] = null;
116
+ embeddedValues = {};
117
+ initialValues = null;
118
+ loaded = true;
119
+ }
120
+
121
+ function getCollectionIcon(name: string) {
122
+ const iconName = ctx.meta.collections[name]?.ui?.icon;
123
+ return iconName && iconName in icons ? (icons as any)[iconName] : Table;
124
+ }
125
+ </script>
126
+
127
+ <FieldWrapper span={2} {isDisabled} {errorMessages}>
128
+ {#snippet children()}
129
+ <div class="rounded-md border bg-muted/30 flex flex-col {destructive ? 'border-destructive' : ''}">
130
+ {#if showForm}
131
+ <div class="flex items-center justify-between gap-2 px-4 py-2.5 border-b">
132
+ <div class="flex items-center gap-2">
133
+ <CollectionIcon size={14} class="text-muted-foreground" />
134
+ <span class="text-sm font-medium">{header?.text ?? ''}</span>
135
+ {#if selectedCollection}
136
+ <span class="text-[0.7rem] text-muted-foreground">{selectedCollection}</span>
137
+ {/if}
138
+ </div>
139
+ {#if !isDisabled}
140
+ <Button onclick={handleClear} variant="outline" class="aspect-square h-6 w-6 shrink-0 p-0" Icon={Ban} tabindex={-1} />
141
+ {/if}
142
+ </div>
143
+ {#if selectedCollection && loaded}
144
+ <DetailView
145
+ collectionName={selectedCollection}
146
+ bind:entry={embeddedValues}
147
+ fieldsErrors={errorMessages?.details ?? {}}
148
+ />
149
+ {/if}
150
+ {:else}
151
+ <div class="flex flex-col items-center gap-3 py-8 px-4 text-center">
152
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
153
+ <GitFork size={20} class="text-muted-foreground" />
154
+ </div>
155
+ <div class="flex flex-col items-center gap-2">
156
+ <div class="flex items-center gap-2">
157
+ <span class="text-sm font-semibold">No record attached in</span>
158
+ <span class="rounded-md border bg-background px-2.5 py-0.5 text-xs font-semibold tracking-wide">{header?.text ?? 'content'}</span>
159
+ </div>
160
+ <span class="text-xs text-muted-foreground">Create a new record to link it here.</span>
161
+ </div>
162
+ {#if !isDisabled}
163
+ <Popover.Root>
164
+ <Popover.Trigger>
165
+ {#snippet child({ props })}
166
+ <button
167
+ {...props}
168
+ class="mt-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
169
+ >
170
+ <Plus size={14} />
171
+ Create
172
+ <ChevronDown size={12} class="opacity-70" />
173
+ </button>
174
+ {/snippet}
175
+ </Popover.Trigger>
176
+ <Popover.Content class="w-48 p-2">
177
+ <div class="flex flex-col gap-1">
178
+ {#each targetCollections as name}
179
+ {@const Icon = getCollectionIcon(name)}
180
+ <Button
181
+ variant="ghost"
182
+ class="justify-start text-xs font-normal h-7 px-2 gap-2"
183
+ onclick={() => handleSelect(name)}
184
+ >
185
+ <Icon size={12} />
186
+ {name}
187
+ </Button>
188
+ {/each}
189
+ </div>
190
+ </Popover.Content>
191
+ </Popover.Root>
192
+ {/if}
193
+ </div>
194
+ {/if}
195
+ </div>
196
+ {/snippet}
197
+ </FieldWrapper>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ virtualField: string;
3
+ collectionField: string;
4
+ idField: string;
5
+ targetCollections: string[];
6
+ entry: Record<string, any>;
7
+ isDisabled?: boolean;
8
+ errorMessages?: any;
9
+ header?: any;
10
+ }
11
+ declare const EmbeddedPolymorphicField: import("svelte").Component<Props, {}, "entry">;
12
+ type EmbeddedPolymorphicField = ReturnType<typeof EmbeddedPolymorphicField>;
13
+ export default EmbeddedPolymorphicField;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { EnumOption } from "@lobb-js/core";
4
+ import * as Select from "../../ui/select/index";
5
+ import EnumBadge from "../../dataTable/enumBadge.svelte";
6
+ import FieldWrapper from "./FieldWrapper.svelte";
7
+
8
+ interface Props {
9
+ value?: any;
10
+ rawEnum: (string | number | EnumOption)[];
11
+ isNumeric?: boolean;
12
+ placeholder?: string;
13
+ destructive?: boolean;
14
+ changedClass?: string;
15
+ isDisabled?: boolean;
16
+ errorMessages?: any;
17
+ header?: any;
18
+ clearBtn?: Snippet<[]>;
19
+ }
20
+ let { value = $bindable(), rawEnum, isNumeric = false, placeholder = "NULL", destructive = false, changedClass = '', isDisabled = false, errorMessages = [], header, clearBtn }: Props = $props();
21
+
22
+ const isEnumOption = rawEnum.length > 0 && typeof rawEnum[0] === "object";
23
+ const enumOptions = isEnumOption ? rawEnum as EnumOption[] : undefined;
24
+ </script>
25
+
26
+ <FieldWrapper span={1} {isDisabled} {errorMessages} {header}>
27
+ {#snippet children()}
28
+ <div class="relative">
29
+ {#if clearBtn}
30
+ <div class="absolute right-1 inset-y-0 z-10 flex items-center">
31
+ {@render clearBtn()}
32
+ </div>
33
+ {/if}
34
+ <Select.Root
35
+ type="single"
36
+ bind:value={
37
+ () => value != null ? String(value) : undefined,
38
+ (v) => {
39
+ if (v == null) { value = null; return; }
40
+ value = isNumeric ? Number(v) : v;
41
+ }
42
+ }
43
+ >
44
+ <Select.Trigger class="h-9 w-full bg-muted pr-8 {changedClass} {destructive ? 'border-destructive' : ''}">
45
+ {#if value != null && enumOptions}
46
+ <EnumBadge value={String(value)} enum={enumOptions} />
47
+ {:else if value != null}
48
+ {value}
49
+ {:else}
50
+ <span class="text-muted-foreground">{placeholder}</span>
51
+ {/if}
52
+ </Select.Trigger>
53
+ <Select.Content>
54
+ <Select.Group>
55
+ {#each rawEnum as option}
56
+ {@const optionValue = typeof option === "object" ? String(option.value) : String(option)}
57
+ <Select.Item value={optionValue} label={optionValue}>
58
+ {#if enumOptions}
59
+ <EnumBadge value={optionValue} enum={enumOptions} />
60
+ {:else}
61
+ {optionValue}
62
+ {/if}
63
+ </Select.Item>
64
+ {/each}
65
+ </Select.Group>
66
+ </Select.Content>
67
+ </Select.Root>
68
+ </div>
69
+ {/snippet}
70
+ </FieldWrapper>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { EnumOption } from "@lobb-js/core";
3
+ interface Props {
4
+ value?: any;
5
+ rawEnum: (string | number | EnumOption)[];
6
+ isNumeric?: boolean;
7
+ placeholder?: string;
8
+ destructive?: boolean;
9
+ changedClass?: string;
10
+ isDisabled?: boolean;
11
+ errorMessages?: any;
12
+ header?: any;
13
+ clearBtn?: Snippet<[]>;
14
+ }
15
+ declare const EnumField: import("svelte").Component<Props, {}, "value">;
16
+ type EnumField = ReturnType<typeof EnumField>;
17
+ export default EnumField;