includio-cms 0.0.30 → 0.0.32

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.
@@ -13,6 +13,8 @@
13
13
  {#await entryQuery}
14
14
  Loading...
15
15
  {:then entry}
16
- <Entry {entry} />
16
+ {#if entry}
17
+ <Entry {entry} />
18
+ {/if}
17
19
  {/await}
18
20
  {/key}
@@ -13,11 +13,7 @@
13
13
  import FileField from './file-field.svelte';
14
14
  import NumberField from './number-field.svelte';
15
15
  import SeoField from './seo-field.svelte';
16
- import UrlField from './url-field.svelte';
17
16
  import RelationField from './relation-field.svelte';
18
- import { urlFieldDataSchema } from '../../../schemas/field/url.js';
19
- import { getAtPath, setAtPath } from '../../utils/objectPath.js';
20
- import { onMount } from 'svelte';
21
17
  import UrlFieldWrapper from './url-field-wrapper.svelte';
22
18
 
23
19
  type Props = {
@@ -14,13 +14,14 @@
14
14
  import Button, { buttonVariants } from '../../../components/ui/button/button.svelte';
15
15
  import { getCollectionEntryLabel } from '../../utils/entryLabel.js';
16
16
  import { getContentLanguage } from '../../state/content-language.svelte.js';
17
- import { tick } from 'svelte';
17
+ import { onMount, tick } from 'svelte';
18
18
  import { useId } from 'bits-ui';
19
19
  import * as Popover from '../../../components/ui/popover/index.js';
20
20
  import * as Command from '../../../components/ui/command/index.js';
21
21
  import { cn } from '../../../utils.js';
22
22
  import Selector from '@tabler/icons-svelte/icons/selector';
23
23
  import Check from '@tabler/icons-svelte/icons/check';
24
+ import type { CollectionConfigWithType } from '../../../types/collections.js';
24
25
 
25
26
  const remotes = getRemotes();
26
27
 
@@ -32,19 +33,32 @@
32
33
 
33
34
  let { field, form, path, ...props }: Props = $props();
34
35
 
35
- let collectionConfig = await remotes.getCollection(field.collection);
36
-
37
36
  const { value } = formFieldProxy(form, path) satisfies FormFieldProxy<RelationFieldData>;
38
37
 
39
- async function getOptionsQuery(): Promise<{ label: string; value: string }[]> {
38
+ let options = $state<{ label: string; value: string }[]>([]);
39
+ let collectionConfig = $state<CollectionConfigWithType | null>(null);
40
+
41
+ async function getCollectionConfig() {
42
+ collectionConfig = await remotes.getCollection(field.collection);
43
+
44
+ return collectionConfig;
45
+ }
46
+
47
+ async function getOptionsQuery(config: CollectionConfigWithType) {
40
48
  const entries = await remotes.getRawCollectionEntries(field.collection);
41
49
 
42
- return entries.map((entry) => ({
43
- label: getCollectionEntryLabel(entry, collectionConfig, getContentLanguage()),
50
+ options = entries.map((entry) => ({
51
+ label: getCollectionEntryLabel(entry, config, getContentLanguage()),
44
52
  value: entry.id
45
53
  }));
46
54
  }
47
55
 
56
+ onMount(() => {
57
+ getCollectionConfig().then((config) => {
58
+ getOptionsQuery(config);
59
+ });
60
+ });
61
+
48
62
  let open = $state(false);
49
63
 
50
64
  // We want to refocus the trigger button when the user selects
@@ -59,7 +73,7 @@
59
73
  const triggerId = useId();
60
74
  </script>
61
75
 
62
- {#await getOptionsQuery() then options}
76
+ {#if options.length > 0 && collectionConfig}
63
77
  <Popover.Root bind:open>
64
78
  <Popover.Trigger
65
79
  id={triggerId}
@@ -102,4 +116,4 @@
102
116
  </Command.Root>
103
117
  </Popover.Content>
104
118
  </Popover.Root>
105
- {/await}
119
+ {/if}
@@ -1,6 +1,6 @@
1
1
  import type { RelationFieldData, RelationField } from '../../../types/fields.js';
2
2
  import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
3
- declare function $$render<T extends Record<string, unknown>>(): Promise<{
3
+ declare function $$render<T extends Record<string, unknown>>(): {
4
4
  props: {
5
5
  field: RelationField;
6
6
  form: SuperForm<T>;
@@ -10,13 +10,13 @@ declare function $$render<T extends Record<string, unknown>>(): Promise<{
10
10
  bindings: "";
11
11
  slots: {};
12
12
  events: {};
13
- }>;
13
+ };
14
14
  declare class __sveltets_Render<T extends Record<string, unknown>> {
15
- props(): Awaited<ReturnType<typeof $$render<T>>>['props'];
16
- events(): Awaited<ReturnType<typeof $$render<T>>>['events'];
17
- slots(): Awaited<ReturnType<typeof $$render<T>>>['slots'];
15
+ props(): ReturnType<typeof $$render<T>>['props'];
16
+ events(): ReturnType<typeof $$render<T>>['events'];
17
+ slots(): ReturnType<typeof $$render<T>>['slots'];
18
18
  bindings(): "";
19
- exports(): Promise<{}>;
19
+ exports(): {};
20
20
  }
21
21
  interface $$IsomorphicComponent {
22
22
  new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
@@ -13,6 +13,7 @@
13
13
  import { joinPath } from '../../utils/objectPath.js';
14
14
  import type {
15
15
  Field,
16
+ ImageField,
16
17
  ObjectFieldData,
17
18
  SeoField,
18
19
  SeoFieldData,
@@ -29,8 +30,6 @@
29
30
 
30
31
  let { field, form, path, ...props }: Props = $props();
31
32
 
32
- // const { value } = formFieldProxy(form, path) satisfies FormFieldProxy<SeoFieldData | undefined>;
33
-
34
33
  const slugField: TextField = {
35
34
  type: 'text',
36
35
  slug: 'slug',
@@ -38,6 +37,13 @@
38
37
  required: true
39
38
  };
40
39
 
40
+ const canonicalUrlField: TextField = {
41
+ type: 'text',
42
+ slug: 'canonicalUrl',
43
+ label: 'Canonical Url',
44
+ required: false
45
+ };
46
+
41
47
  const titleField: TextField = {
42
48
  type: 'text',
43
49
  slug: 'title',
@@ -53,7 +59,38 @@
53
59
  multiline: true
54
60
  };
55
61
 
56
- const fields: Field[] = [slugField, titleField, descriptionField];
62
+ const keyWordsField: TextField = {
63
+ type: 'text',
64
+ slug: 'keywords',
65
+ label: 'Keywords',
66
+ required: false,
67
+ multiline: true
68
+ };
69
+
70
+ const ogImageField: ImageField = {
71
+ type: 'image',
72
+ slug: 'ogImage',
73
+ label: 'Open Graph Image',
74
+ required: false
75
+ };
76
+
77
+ const customCodeField: TextField = {
78
+ type: 'text',
79
+ slug: 'customCode',
80
+ label: 'Custom Code',
81
+ required: false,
82
+ multiline: true
83
+ };
84
+
85
+ const fields: Field[] = [
86
+ slugField,
87
+ canonicalUrlField,
88
+ titleField,
89
+ descriptionField,
90
+ keyWordsField,
91
+ ogImageField,
92
+ customCodeField
93
+ ];
57
94
  </script>
58
95
 
59
96
  <Item.Root variant="outline">
@@ -42,12 +42,6 @@
42
42
  let fieldValid = $state(false);
43
43
  </script>
44
44
 
45
- <svelte:boundary>
46
- {#snippet pending()}
47
- Loading...
48
- {/snippet}
49
-
50
- {#if fieldValid}
51
- <UrlFieldComponent {form} {field} path={path as FormPathLeaves<T, UrlFieldData>} {...props} />
52
- {/if}
53
- </svelte:boundary>
45
+ {#if fieldValid}
46
+ <UrlFieldComponent {form} {field} path={path as FormPathLeaves<T, UrlFieldData>} {...props} />
47
+ {/if}
@@ -23,16 +23,15 @@
23
23
  let { selected = $bindable([]), multiple = false, accept }: Props = $props();
24
24
 
25
25
  let currentFile: MediaFile | null = $state(null);
26
-
27
26
  let selectedFolder = $state<string | null>(null);
28
27
 
29
- const filesQuery = $derived(
30
- remotes.getMediaFiles({
31
- data: { folderId: selectedFolder || undefined, mimeTypes: accept?.split(',') }
32
- })
33
- );
28
+ async function fetchFiles(folderId: string | null, accept: string | undefined) {
29
+ return await remotes.getMediaFiles({
30
+ data: { folderId: folderId || undefined, mimeTypes: accept?.split(',') }
31
+ });
32
+ }
34
33
 
35
- let files = $derived(filesQuery.current);
34
+ let files = $derived(await fetchFiles(selectedFolder, accept));
36
35
 
37
36
  let folders = $derived(await remotes.getMediaFolders());
38
37
 
@@ -40,7 +39,7 @@
40
39
  if (currentFile) {
41
40
  await remotes.deleteMediaFile(currentFile.id);
42
41
  toast.success('File deleted');
43
- filesQuery.refresh();
42
+ files = await fetchFiles(selectedFolder, accept);
44
43
  currentFile = null;
45
44
  }
46
45
  }
@@ -48,18 +47,8 @@
48
47
  onMount(() => {
49
48
  if (Array.isArray(selected)) {
50
49
  selected = selected.filter((id) => !id.startsWith('/uploads'));
51
- if (files) {
52
- currentFile = files.find((file) => file.id === selected[0]) || null;
53
- }
54
50
  } else if (typeof selected === 'string') {
55
51
  selected = selected.startsWith('/uploads') ? '' : selected;
56
- if (files) {
57
- currentFile = files.find((file) => file.id === selected) || null;
58
- }
59
- } else {
60
- if (files) {
61
- currentFile = files[0];
62
- }
63
52
  }
64
53
  });
65
54
  </script>
@@ -94,52 +83,44 @@
94
83
  </div>
95
84
 
96
85
  <div>
97
- {#await filesQuery}
98
- <div class="text-center text-gray-500">Loading...</div>
99
- {:then files}
100
- {#if !files.length}
101
- <div class="text-center text-gray-500">No files found</div>
102
- {/if}
103
-
104
- <div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2">
105
- {#each files as file}
106
- <button
107
- type="button"
108
- class="block aspect-square overflow-hidden rounded-2xl border p-1 {selected.includes(
109
- file.id
110
- )
111
- ? 'outline-primary outline-4'
112
- : ''}"
113
- onclick={() => {
114
- currentFile = file;
115
- if (multiple && Array.isArray(selected)) {
116
- selected = selected.includes(file.id)
117
- ? selected.filter((id) => id !== file.id)
118
- : [...selected, file.id];
119
- } else {
120
- selected = file.id;
121
- }
122
- }}
123
- >
124
- {#if file.type === 'image'}
125
- <img
126
- class="pointer-events-none h-full w-full object-contain"
127
- src={file.url}
128
- alt={file.name}
129
- />
130
- {:else if file.type === 'video'}
131
- <img
132
- class="pointer-events-none h-full w-full object-contain"
133
- src={file.thumbnailUrl}
134
- alt={file.name}
135
- />
136
- {:else}
137
- <Pdf class="pointer-events-none h-full w-full object-contain" />
138
- {/if}
139
- </button>
140
- {/each}
141
- </div>
142
- {/await}
86
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2">
87
+ {#each files as file}
88
+ <button
89
+ type="button"
90
+ class="block aspect-square overflow-hidden rounded-2xl border p-1 {selected.includes(
91
+ file.id
92
+ )
93
+ ? 'outline-primary outline-4'
94
+ : ''}"
95
+ onclick={() => {
96
+ currentFile = file;
97
+ if (multiple && Array.isArray(selected)) {
98
+ selected = selected.includes(file.id)
99
+ ? selected.filter((id) => id !== file.id)
100
+ : [...selected, file.id];
101
+ } else {
102
+ selected = file.id;
103
+ }
104
+ }}
105
+ >
106
+ {#if file.type === 'image'}
107
+ <img
108
+ class="pointer-events-none h-full w-full object-contain"
109
+ src={file.url}
110
+ alt={file.name}
111
+ />
112
+ {:else if file.type === 'video'}
113
+ <img
114
+ class="pointer-events-none h-full w-full object-contain"
115
+ src={file.thumbnailUrl}
116
+ alt={file.name}
117
+ />
118
+ {:else}
119
+ <Pdf class="pointer-events-none h-full w-full object-contain" />
120
+ {/if}
121
+ </button>
122
+ {/each}
123
+ </div>
143
124
  </div>
144
125
  </div>
145
126
 
@@ -147,8 +128,12 @@
147
128
  <Item.Content class="h-full">
148
129
  {#if currentFile}
149
130
  <div class="flex items-center justify-end border-b px-6 py-3">
150
- <Button size="sm" class="mr-2.5" variant="destructive" onclick={deleteFileCommand}
151
- >Delete</Button
131
+ <Button
132
+ type="button"
133
+ size="sm"
134
+ class="mr-2.5"
135
+ variant="destructive"
136
+ onclick={deleteFileCommand}>Delete</Button
152
137
  >
153
138
  </div>
154
139
  <div class="space-y-6 p-6">
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- import { Dialog as DialogPrimitive } from "bits-ui";
3
- import XIcon from "@lucide/svelte/icons/x";
4
- import type { Snippet } from "svelte";
5
- import * as Dialog from "./index.js";
6
- import { cn, type WithoutChildrenOrChild } from "../../../utils.js";
2
+ import { Dialog as DialogPrimitive } from 'bits-ui';
3
+ import XIcon from '@lucide/svelte/icons/x';
4
+ import type { Snippet } from 'svelte';
5
+ import * as Dialog from './index.js';
6
+ import { cn, type WithoutChildrenOrChild } from '../../../utils.js';
7
7
 
8
8
  let {
9
9
  ref = $bindable(null),
@@ -25,7 +25,7 @@
25
25
  bind:ref
26
26
  data-slot="dialog-content"
27
27
  class={cn(
28
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
28
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
29
29
  className
30
30
  )}
31
31
  {...restProps}
@@ -33,7 +33,7 @@
33
33
  {@render children?.()}
34
34
  {#if showCloseButton}
35
35
  <DialogPrimitive.Close
36
- class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
36
+ class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
37
37
  >
38
38
  <XIcon />
39
39
  <span class="sr-only">Close</span>
@@ -1,6 +1,6 @@
1
- import { Dialog as DialogPrimitive } from "bits-ui";
2
- import type { Snippet } from "svelte";
3
- import { type WithoutChildrenOrChild } from "../../../utils.js";
1
+ import { Dialog as DialogPrimitive } from 'bits-ui';
2
+ import type { Snippet } from 'svelte';
3
+ import { type WithoutChildrenOrChild } from '../../../utils.js';
4
4
  type $$ComponentProps = WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
5
5
  portalProps?: DialogPrimitive.PortalProps;
6
6
  children: Snippet;
@@ -87,8 +87,12 @@ export function generateZodSchemaFromField(field, languages, options = {
87
87
  case 'seo': {
88
88
  return z.object({
89
89
  slug: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
90
+ canonicalUrl: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()]))),
90
91
  title: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
91
- description: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()])))
92
+ description: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
93
+ keywords: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()]))),
94
+ ogImage: z.string(),
95
+ customCode: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()])))
92
96
  });
93
97
  }
94
98
  case 'url': {
@@ -47,8 +47,12 @@ function getFieldTypeAsString(field) {
47
47
  case 'seo':
48
48
  return `{
49
49
  slug?: string;
50
+ canonicalUrl?: string;
50
51
  title?: string;
51
52
  description?: string;
53
+ ogImage?: string;
54
+ keywords?: string;
55
+ customCode?: string;
52
56
  }`;
53
57
  case 'url':
54
58
  return 'string';
@@ -58,6 +62,6 @@ function getFieldTypeAsString(field) {
58
62
  }
59
63
  export function generateTsTypeFromFields(fields) {
60
64
  return `{
61
- ${fields.map((f) => ` ${f.slug}${f.required ? '' : '?'}: ${getFieldTypeAsString(f)}`).join(';\n')};
65
+ ${fields.map((f) => ` ${f.slug}${f.required || f.type === 'seo' ? '' : '?'}: ${getFieldTypeAsString(f)}`).join(';\n')};
62
66
  }`;
63
67
  }
@@ -1,12 +1,12 @@
1
1
  <script lang="ts">
2
2
  import type { MediaFile } from '../../types/media.js';
3
+ import type { HTMLImgAttributes } from 'svelte/elements';
3
4
 
4
- type Props = {
5
- class?: string;
5
+ type Props = HTMLImgAttributes & {
6
6
  data: MediaFile;
7
7
  };
8
8
 
9
- let { class: className = undefined, data }: Props = $props();
9
+ let { class: className = undefined, data, ...restProps }: Props = $props();
10
10
  </script>
11
11
 
12
12
  <img
@@ -16,4 +16,5 @@
16
16
  width={data.width}
17
17
  height={data.height}
18
18
  loading="lazy"
19
+ {...restProps}
19
20
  />
@@ -1,6 +1,6 @@
1
1
  import type { MediaFile } from '../../types/media.js';
2
- type Props = {
3
- class?: string;
2
+ import type { HTMLImgAttributes } from 'svelte/elements';
3
+ type Props = HTMLImgAttributes & {
4
4
  data: MediaFile;
5
5
  };
6
6
  declare const Image: import("svelte").Component<Props, {}, "">;
@@ -1,10 +1,7 @@
1
1
  <script lang="ts">
2
- type Props = {
3
- title?: string;
4
- description?: string;
5
- };
2
+ import type { SeoFieldData } from '../../types/fields.js';
6
3
 
7
- let { title, description }: Props = $props();
4
+ let { title, description, keywords, ogImage, customCode, canonicalUrl }: SeoFieldData = $props();
8
5
  </script>
9
6
 
10
7
  <svelte:head>
@@ -15,4 +12,23 @@
15
12
  {#if description}
16
13
  <meta name="description" content={description} />
17
14
  {/if}
15
+ {#if keywords}
16
+ <meta name="keywords" content={keywords} />
17
+ {/if}
18
+ {#if canonicalUrl}
19
+ <link rel="canonical" href={canonicalUrl} />
20
+ {/if}
21
+ {#if title}
22
+ <meta property="og:title" content={title} />
23
+ {/if}
24
+ {#if description}
25
+ <meta property="og:description" content={description} />
26
+ {/if}
27
+ {#if ogImage}
28
+ <meta property="og:image" content={ogImage} />
29
+ <meta name="twitter:card" content="summary_large_image" />
30
+ {/if}
31
+ {#if customCode}
32
+ {@html customCode}
33
+ {/if}
18
34
  </svelte:head>
@@ -1,7 +1,4 @@
1
- type Props = {
2
- title?: string;
3
- description?: string;
4
- };
5
- declare const Seo: import("svelte").Component<Props, {}, "">;
1
+ import type { SeoFieldData } from '../../types/fields.js';
2
+ declare const Seo: import("svelte").Component<SeoFieldData, {}, "">;
6
3
  type Seo = ReturnType<typeof Seo>;
7
4
  export default Seo;
@@ -122,9 +122,13 @@ export interface SeoField extends BaseField {
122
122
  type: 'seo';
123
123
  }
124
124
  export interface SeoFieldData {
125
- slug: Record<string, string>;
126
- title: Record<string, string>;
127
- description: Record<string, string>;
125
+ slug: string;
126
+ canonicalUrl?: string;
127
+ title: string;
128
+ description?: string;
129
+ ogImage?: string;
130
+ keywords?: string;
131
+ customCode?: string;
128
132
  }
129
133
  export interface UrlField extends BaseField {
130
134
  type: 'url';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -144,7 +144,8 @@
144
144
  "typescript": "^5.0.0",
145
145
  "typescript-eslint": "^8.20.0",
146
146
  "vite": "^7.0.4",
147
- "vitest": "^3.2.3"
147
+ "vitest": "^3.2.3",
148
+ "drizzle-orm": "^0.40.0"
148
149
  },
149
150
  "keywords": [
150
151
  "svelte"
@@ -153,21 +154,17 @@
153
154
  "@inlang/paraglide-js": "^2.0.0",
154
155
  "@internationalized/date": "^3.8.2",
155
156
  "@lucide/svelte": "^0.544.0",
156
- "@node-rs/argon2": "^2.0.2",
157
157
  "@oslojs/crypto": "^1.0.1",
158
158
  "@oslojs/encoding": "^1.1.0",
159
159
  "@tabler/icons-svelte": "^3.34.0",
160
- "@tanstack/svelte-query": "^5.82.0",
161
- "@tanstack/svelte-query-devtools": "^5.84.0",
162
160
  "@tanstack/table-core": "^8.21.3",
163
161
  "@tiptap/core": "^3.4.4",
164
162
  "@tiptap/extension-bubble-menu": "^3.4.4",
165
163
  "@tiptap/pm": "^3.4.4",
166
164
  "@tiptap/starter-kit": "^3.4.4",
167
165
  "arctic": "^3.7.0",
168
- "bits-ui": "^2.11.0",
166
+ "bits-ui": "^2.11.5",
169
167
  "dotenv": "^16.5.0",
170
- "drizzle-orm": "^0.40.0",
171
168
  "fast-glob": "^3.3.3",
172
169
  "fluent-ffmpeg": "^2.1.3",
173
170
  "mode-watcher": "^1.0.8",