includio-cms 0.5.2 → 0.5.5

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 (132) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/ROADMAP.md +29 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +7 -7
  18. package/dist/admin/client/collection/collection-entries.svelte +56 -5
  19. package/dist/admin/client/collection/data-table.svelte +127 -18
  20. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  21. package/dist/admin/client/entry/entry-form.svelte +1 -0
  22. package/dist/admin/client/entry/entry.svelte +130 -123
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  24. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  25. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  26. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  27. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  28. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  29. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  30. package/dist/admin/components/fields/content-field.svelte +4 -17
  31. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  32. package/dist/admin/components/fields/date-field.svelte +8 -21
  33. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  34. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  35. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  36. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  37. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  38. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  39. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  40. package/dist/admin/components/fields/fields-form.svelte +13 -10
  41. package/dist/admin/components/fields/file-field.svelte +12 -27
  42. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  43. package/dist/admin/components/fields/image-field.svelte +13 -28
  44. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/media-field.svelte +15 -30
  46. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  47. package/dist/admin/components/fields/number-field.svelte +6 -20
  48. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  49. package/dist/admin/components/fields/object-field.svelte +26 -29
  50. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  51. package/dist/admin/components/fields/radio-field.svelte +8 -20
  52. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/fields/relation-field.svelte +28 -40
  54. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  55. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  56. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  57. package/dist/admin/components/fields/select-field.svelte +14 -28
  58. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  59. package/dist/admin/components/fields/seo-field.svelte +5 -12
  60. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  61. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  62. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  63. package/dist/admin/components/fields/slug-field.svelte +6 -11
  64. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  65. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  66. package/dist/admin/components/fields/text-field.svelte +7 -19
  67. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  68. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  69. package/dist/admin/components/fields/url-field.svelte +294 -128
  70. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  71. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  72. package/dist/admin/components/layout/nav-collections.svelte +2 -1
  73. package/dist/admin/components/layout/nav-forms.svelte +2 -1
  74. package/dist/admin/components/layout/nav-singletons.svelte +2 -1
  75. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  76. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  77. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  78. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  79. package/dist/admin/components/tiptap/slash-command.js +2 -3
  80. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  81. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  82. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  83. package/dist/admin/remote/entry.remote.d.ts +9 -1
  84. package/dist/admin/remote/entry.remote.js +30 -2
  85. package/dist/admin/styles/admin.css +10 -0
  86. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  87. package/dist/admin/utils/fieldCondition.js +20 -0
  88. package/dist/cli/scaffold/admin.js +8 -0
  89. package/dist/components/ui/switch/index.d.ts +2 -0
  90. package/dist/components/ui/switch/index.js +4 -0
  91. package/dist/components/ui/switch/switch.svelte +26 -0
  92. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  93. package/dist/core/cms.d.ts +2 -1
  94. package/dist/core/cms.js +2 -0
  95. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  96. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  97. package/dist/core/fields/urlUtils.d.ts +14 -0
  98. package/dist/core/fields/urlUtils.js +21 -0
  99. package/dist/core/server/entries/operations/get.js +2 -1
  100. package/dist/core/server/entries/operations/update.d.ts +1 -0
  101. package/dist/core/server/entries/operations/update.js +5 -1
  102. package/dist/core/server/fields/populateEntry.js +43 -0
  103. package/dist/core/server/fields/resolveImageFields.js +33 -1
  104. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  105. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  106. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  107. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  108. package/dist/core/server/generator/formFields.js +2 -0
  109. package/dist/core/server/generator/generator.js +25 -1
  110. package/dist/db-postgres/schema/entry.d.ts +17 -0
  111. package/dist/db-postgres/schema/entry.js +4 -2
  112. package/dist/schemas/field/url.d.ts +2 -0
  113. package/dist/schemas/field/url.js +4 -2
  114. package/dist/server/auth.d.ts +6 -6
  115. package/dist/sveltekit/server/handle.js +1 -0
  116. package/dist/types/cms.d.ts +7 -0
  117. package/dist/types/collections.d.ts +2 -0
  118. package/dist/types/entries.d.ts +7 -1
  119. package/dist/types/fields.d.ts +9 -0
  120. package/dist/types/formFields.d.ts +15 -2
  121. package/dist/types/index.d.ts +2 -1
  122. package/dist/types/index.js +1 -0
  123. package/dist/updates/0.5.3/index.d.ts +2 -0
  124. package/dist/updates/0.5.3/index.js +19 -0
  125. package/dist/updates/0.5.4/index.d.ts +2 -0
  126. package/dist/updates/0.5.4/index.js +15 -0
  127. package/dist/updates/0.5.5/index.d.ts +2 -0
  128. package/dist/updates/0.5.5/index.js +20 -0
  129. package/dist/updates/index.js +4 -1
  130. package/package.json +7 -1
  131. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  132. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
@@ -1,25 +1,144 @@
1
1
  <script lang="ts">
2
2
  import { NodeViewWrapper } from 'svelte-tiptap';
3
3
  import type { NodeViewProps } from '@tiptap/core';
4
- import StandaloneFieldRenderer from '../fields/standalone-field-renderer.svelte';
5
- import type { ObjectField } from '../../../types/fields.js';
4
+ import FieldRenderer from '../fields/field-renderer.svelte';
5
+ import { createStandaloneForm } from './standalone-form.js';
6
+ import type { ObjectField, BlocksField, Field, FieldType } from '../../../types/fields.js';
7
+ import { getContentLanguage } from '../../state/content-language.svelte.js';
6
8
  import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
7
9
  import ArrowUp from '@tabler/icons-svelte/icons/arrow-up';
8
10
  import ArrowDown from '@tabler/icons-svelte/icons/arrow-down';
9
11
  import Trash from '@tabler/icons-svelte/icons/trash';
12
+ import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
13
+ import { slide } from 'svelte/transition';
14
+ import { onMount, onDestroy } from 'svelte';
15
+ import type { FormPathLeaves } from 'sveltekit-superforms';
16
+ import { evaluateCondition } from '../../utils/fieldCondition.js';
10
17
 
11
18
  let { node, updateAttributes, editor, getPos, deleteNode, selected }: NodeViewProps = $props();
12
19
 
20
+ let collapsed = $state(false);
21
+
22
+ const contentLanguage = getContentLanguage();
23
+
24
+ const SKIP_TYPES: Set<FieldType> = new Set(['slug', 'seo']);
25
+
26
+ function normalizeBlockData(
27
+ data: Record<string, unknown>,
28
+ fields: Field[],
29
+ langs: string[]
30
+ ): Record<string, unknown> {
31
+ const result = { ...data };
32
+ for (const f of fields) {
33
+ const key = f.slug;
34
+ if (['text', 'richtext', 'content'].includes(f.type)) {
35
+ if (typeof result[key] !== 'object' || result[key] == null) {
36
+ const val = result[key] ?? (f.type === 'content' ? null : '');
37
+ result[key] = Object.fromEntries(langs.map((l) => [l, val]));
38
+ }
39
+ } else if (f.type === 'url') {
40
+ const v = result[key] as Record<string, unknown> | undefined;
41
+ if (!v || typeof v !== 'object') {
42
+ result[key] = { id: '', url: {} };
43
+ } else if (!('url' in v) || v.url == null) {
44
+ result[key] = { ...v, url: {} };
45
+ } else if (typeof v.url === 'string') {
46
+ const urlStr = v.url as string;
47
+ const textStr = typeof v.text === 'string' ? v.text : '';
48
+ result[key] = {
49
+ ...v,
50
+ url: Object.fromEntries(langs.map((l) => [l, urlStr])),
51
+ text: Object.fromEntries(langs.map((l) => [l, textStr]))
52
+ };
53
+ }
54
+ const urlField = f as import('../../../types/fields.js').UrlField;
55
+ const d = result[key] as Record<string, unknown>;
56
+ if (urlField.text && !d.text) d.text = {};
57
+ if (urlField.newTab && d.newTab === undefined) d.newTab = false;
58
+ if (urlField.rel && d.rel === undefined) d.rel = '';
59
+ } else if (f.type === 'blocks') {
60
+ const bf = f as BlocksField;
61
+ if (Array.isArray(result[key])) {
62
+ result[key] = (result[key] as any[]).map((item) => ({
63
+ ...item,
64
+ data: normalizeBlockData(
65
+ item.data ?? {},
66
+ bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
67
+ langs
68
+ )
69
+ }));
70
+ }
71
+ } else if (f.type === 'object') {
72
+ const of_ = f as ObjectField;
73
+ if (result[key] && typeof result[key] === 'object' && 'data' in (result[key] as object)) {
74
+ const obj = result[key] as Record<string, unknown>;
75
+ result[key] = { ...obj, data: normalizeBlockData(obj.data as Record<string, unknown>, of_.fields, langs) };
76
+ }
77
+ }
78
+ }
79
+ return result;
80
+ }
81
+
82
+ function denormalizeBlockData(
83
+ data: Record<string, unknown>,
84
+ fields: Field[],
85
+ currentLang: string
86
+ ): Record<string, unknown> {
87
+ const result = { ...data };
88
+ for (const f of fields) {
89
+ const key = f.slug;
90
+ const val = result[key];
91
+ if (['text', 'richtext'].includes(f.type)) {
92
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
93
+ result[key] = (val as Record<string, unknown>)[currentLang] ?? '';
94
+ }
95
+ } else if (f.type === 'url' && val && typeof val === 'object') {
96
+ const v = val as Record<string, unknown>;
97
+ const flat: Record<string, unknown> = {};
98
+ if (v.id !== undefined) flat.id = v.id;
99
+ flat.url =
100
+ v.url && typeof v.url === 'object'
101
+ ? (v.url as Record<string, string>)[currentLang] ?? ''
102
+ : v.url ?? '';
103
+ if ('text' in v) {
104
+ flat.text =
105
+ v.text && typeof v.text === 'object'
106
+ ? (v.text as Record<string, string>)[currentLang] ?? ''
107
+ : v.text ?? '';
108
+ }
109
+ if (v.newTab !== undefined) flat.newTab = v.newTab;
110
+ if (v.rel !== undefined) flat.rel = v.rel;
111
+ result[key] = flat;
112
+ } else if (f.type === 'blocks') {
113
+ const bf = f as BlocksField;
114
+ if (Array.isArray(val)) {
115
+ result[key] = (val as any[]).map((item) => ({
116
+ ...item,
117
+ data: denormalizeBlockData(
118
+ item.data ?? {},
119
+ bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
120
+ currentLang
121
+ )
122
+ }));
123
+ }
124
+ } else if (f.type === 'object') {
125
+ const of_ = f as ObjectField;
126
+ if (val && typeof val === 'object' && 'data' in (val as object)) {
127
+ const obj = val as Record<string, unknown>;
128
+ result[key] = { ...obj, data: denormalizeBlockData(obj.data as Record<string, unknown>, of_.fields, currentLang) };
129
+ }
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+
13
135
  const inlineBlocks: ObjectField[] = $derived(
14
136
  (editor.extensionManager.extensions.find((e) => e.name === 'inlineBlock')?.options as { inlineBlocks: ObjectField[] })?.inlineBlocks ?? []
15
137
  );
16
138
 
17
139
  const blockDef = $derived(inlineBlocks.find((b) => b.slug === node.attrs.blockType));
18
140
  const blockLabel = $derived(blockDef?.label ? (Object.values(blockDef.label)[0] ?? blockDef.slug) : node.attrs.blockType);
19
-
20
- let blockData = $state<Record<string, unknown>>(parseBlockData(node.attrs.blockData));
21
- // Track last JSON we wrote to PM to avoid re-parsing our own writes
22
- let lastWrittenJson = '';
141
+ const supportedFields = $derived(blockDef?.fields.filter((f) => !SKIP_TYPES.has(f.type)) ?? []);
23
142
 
24
143
  function parseBlockData(raw: unknown): Record<string, unknown> {
25
144
  if (typeof raw === 'string') {
@@ -29,25 +148,41 @@
29
148
  return {};
30
149
  }
31
150
 
32
- // Sync external changes (e.g. undo/redo) — skip if it's our own write
33
- $effect(() => {
34
- const raw = node.attrs.blockData;
35
- const rawJson = typeof raw === 'string' ? raw : JSON.stringify(raw);
36
- if (rawJson !== lastWrittenJson) {
37
- blockData = parseBlockData(raw);
38
- }
39
- });
151
+ const langs = contentLanguage.all;
152
+ const allFields = blockDef?.fields ?? [];
153
+ const standaloneForm = createStandaloneForm(normalizeBlockData(parseBlockData(node.attrs.blockData), allFields, langs));
154
+ const formStore = standaloneForm.form;
40
155
 
156
+ // Track last JSON we wrote to PM to avoid re-parsing our own writes
157
+ let lastWrittenJson = '';
41
158
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
42
159
 
43
- function handleFieldChange(slug: string, value: unknown) {
44
- blockData = { ...blockData, [slug]: value };
160
+ // Form store ProseMirror (user edits)
161
+ const unsubscribe = formStore.subscribe((data) => {
45
162
  if (debounceTimer) clearTimeout(debounceTimer);
46
163
  debounceTimer = setTimeout(() => {
47
- lastWrittenJson = JSON.stringify(blockData);
48
- updateAttributes({ blockData: lastWrittenJson });
164
+ const denormalized = denormalizeBlockData(data, allFields, contentLanguage.current);
165
+ const json = JSON.stringify(denormalized);
166
+ if (json !== lastWrittenJson) {
167
+ lastWrittenJson = json;
168
+ updateAttributes({ blockData: json });
169
+ }
49
170
  }, 300);
50
- }
171
+ });
172
+
173
+ onDestroy(() => {
174
+ unsubscribe();
175
+ if (debounceTimer) clearTimeout(debounceTimer);
176
+ });
177
+
178
+ // ProseMirror → Form store (undo/redo, external changes)
179
+ $effect(() => {
180
+ const raw = node.attrs.blockData;
181
+ const rawJson = typeof raw === 'string' ? raw : JSON.stringify(raw);
182
+ if (rawJson !== lastWrittenJson) {
183
+ formStore.set(normalizeBlockData(parseBlockData(raw), allFields, langs));
184
+ }
185
+ });
51
186
 
52
187
  function moveUp() {
53
188
  const pos = getPos();
@@ -116,7 +251,18 @@
116
251
  <button type="button" class="grip-handle" aria-label="Przeciągnij" data-drag-handle>
117
252
  <GripVertical size={16} />
118
253
  </button>
119
- <span class="inline-block-label">{blockLabel}</span>
254
+ <button
255
+ type="button"
256
+ class="inline-block-collapse-toggle"
257
+ onclick={() => (collapsed = !collapsed)}
258
+ aria-expanded={!collapsed}
259
+ aria-label={collapsed ? 'Rozwiń blok' : 'Zwiń blok'}
260
+ >
261
+ <span class="inline-block-label">{blockLabel}</span>
262
+ <span class="collapse-chevron" class:collapsed>
263
+ <ChevronDown size={14} />
264
+ </span>
265
+ </button>
120
266
  </div>
121
267
  <div class="inline-block-header-actions">
122
268
  <button type="button" class="action-btn" onclick={moveUp} aria-label="Przenieś wyżej" title="Przenieś wyżej">
@@ -132,17 +278,25 @@
132
278
  </div>
133
279
 
134
280
  <!-- Body: field renderer -->
135
- {#if blockDef}
136
- <div class="inline-block-body">
137
- <StandaloneFieldRenderer
138
- fields={blockDef.fields}
139
- data={blockData}
140
- onchange={handleFieldChange}
141
- />
142
- </div>
143
- {:else}
144
- <div class="inline-block-body inline-block-unknown">
145
- Nieznany typ bloku: {node.attrs.blockType}
281
+ {#if !collapsed}
282
+ <div transition:slide={{ duration: 150 }}>
283
+ {#if blockDef}
284
+ <div class="inline-block-body space-y-3">
285
+ {#each supportedFields as f}
286
+ {#if evaluateCondition(f.showWhen, (slug) => $formStore[slug])}
287
+ <FieldRenderer
288
+ field={f}
289
+ form={standaloneForm}
290
+ path={f.slug as FormPathLeaves<Record<string, unknown>>}
291
+ />
292
+ {/if}
293
+ {/each}
294
+ </div>
295
+ {:else}
296
+ <div class="inline-block-body inline-block-unknown">
297
+ Nieznany typ bloku: {node.attrs.blockType}
298
+ </div>
299
+ {/if}
146
300
  </div>
147
301
  {/if}
148
302
  </div>
@@ -175,18 +329,54 @@
175
329
  min-height: 36px;
176
330
  }
177
331
 
332
+ .inline-block-wrapper:has(.collapse-chevron.collapsed) .inline-block-header {
333
+ border-bottom-color: transparent;
334
+ }
335
+
178
336
  .inline-block-header-left {
179
337
  display: flex;
180
338
  align-items: center;
181
339
  gap: 0.25rem;
182
340
  }
183
341
 
342
+ .inline-block-collapse-toggle {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 0.25rem;
346
+ border: none;
347
+ background: transparent;
348
+ cursor: pointer;
349
+ padding: 2px 4px;
350
+ border-radius: 4px;
351
+ color: inherit;
352
+ }
353
+
354
+ .inline-block-collapse-toggle:hover {
355
+ background: var(--accent);
356
+ }
357
+
358
+ .inline-block-collapse-toggle:focus-visible {
359
+ outline: 2px solid var(--ring);
360
+ outline-offset: 1px;
361
+ }
362
+
184
363
  .inline-block-label {
185
364
  font-size: 0.8125rem;
186
365
  font-weight: 600;
187
366
  color: var(--foreground);
188
367
  }
189
368
 
369
+ .collapse-chevron {
370
+ display: flex;
371
+ align-items: center;
372
+ color: var(--muted-foreground);
373
+ transition: transform 0.15s ease;
374
+ }
375
+
376
+ .collapse-chevron.collapsed {
377
+ transform: rotate(-90deg);
378
+ }
379
+
190
380
  .inline-block-header-actions {
191
381
  display: flex;
192
382
  align-items: center;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, getAllContexts } from 'svelte';
3
3
  import { Editor, type Editor as EditorType } from '@tiptap/core';
4
4
  import BubbleMenu from '@tiptap/extension-bubble-menu';
5
5
  import { extensions } from './extensions.js';
@@ -17,6 +17,7 @@
17
17
  import { InlineBlockNode } from './inline-block-node.js';
18
18
  import { SlashCommand } from './slash-command.js';
19
19
  import { HeadingA11yPlugin } from './heading-a11y-plugin.js';
20
+ import Placeholder from '@tiptap/extension-placeholder';
20
21
 
21
22
  // Icons (bubble menu only)
22
23
  import Bold from '@tabler/icons-svelte/icons/bold';
@@ -43,6 +44,10 @@
43
44
 
44
45
  let { value = $bindable(undefined), inlineBlocks = [] }: Props = $props();
45
46
 
47
+ // Capture parent contexts at init time (before onMount) so SvelteNodeViewRenderer
48
+ // can propagate them to inline block node views (needed for getRemotes() etc.)
49
+ const parentContexts = getAllContexts();
50
+
46
51
  const hasInlineBlocks = $derived(inlineBlocks.length > 0);
47
52
 
48
53
  let element = $state<HTMLDivElement | null>(null);
@@ -64,7 +69,7 @@
64
69
 
65
70
  const extraExtensions = hasInlineBlocks
66
71
  ? [
67
- InlineBlockNode.configure({ inlineBlocks }),
72
+ InlineBlockNode.configure({ inlineBlocks, context: parentContexts }),
68
73
  SlashCommand.configure({ inlineBlocks })
69
74
  ]
70
75
  : [];
@@ -77,6 +82,10 @@
77
82
  HeadingA11yPlugin,
78
83
  BubbleMenu.configure({
79
84
  element: bubbleMenu!
85
+ }),
86
+ Placeholder.configure({
87
+ placeholder: 'Wpisz treść lub "/" by wstawić element',
88
+ showOnlyCurrent: true
80
89
  })
81
90
  ],
82
91
  content: initialContent,
@@ -271,6 +280,8 @@
271
280
 
272
281
  <style>
273
282
  .bubble-menu {
283
+ height: 0;
284
+ overflow: visible;
274
285
  visibility: hidden;
275
286
  opacity: 0;
276
287
  transition:
@@ -3,6 +3,7 @@ import type { ObjectField } from '../../../types/fields.js';
3
3
  export interface InlineBlockOptions {
4
4
  HTMLAttributes: Record<string, string>;
5
5
  inlineBlocks: ObjectField[];
6
+ context?: Map<unknown, unknown>;
6
7
  }
7
8
  declare module '@tiptap/core' {
8
9
  interface Commands<ReturnType> {
@@ -17,6 +17,9 @@ function getFieldDefault(field) {
17
17
  return field.defaultValue;
18
18
  switch (field.type) {
19
19
  case 'text':
20
+ case 'richtext':
21
+ case 'date':
22
+ case 'datetime':
20
23
  return '';
21
24
  case 'number':
22
25
  return 0;
@@ -25,8 +28,20 @@ function getFieldDefault(field) {
25
28
  case 'select':
26
29
  case 'radio':
27
30
  return field.options[0]?.value ?? '';
31
+ case 'checkboxes':
32
+ return [];
28
33
  case 'url':
29
34
  return { url: '' };
35
+ case 'image':
36
+ case 'file':
37
+ case 'media':
38
+ case 'relation':
39
+ return field.multiple ? [] : '';
40
+ case 'object':
41
+ return { slug: field.slug, data: {} };
42
+ case 'blocks':
43
+ case 'array':
44
+ return [];
30
45
  default:
31
46
  return '';
32
47
  }
@@ -93,6 +108,8 @@ export const InlineBlockNode = Node.create({
93
108
  };
94
109
  },
95
110
  addNodeView() {
96
- return SvelteNodeViewRenderer(InlineBlockNodeView);
111
+ return SvelteNodeViewRenderer(InlineBlockNodeView, {
112
+ context: this.options.context
113
+ });
97
114
  }
98
115
  });
@@ -104,8 +104,7 @@ export const SlashCommand = Extension.create({
104
104
  if (!query)
105
105
  return allItems;
106
106
  const q = query.toLowerCase();
107
- return allItems.filter((item) => item.label.toLowerCase().includes(q) ||
108
- item.group.toLowerCase().includes(q));
107
+ return allItems.filter((item) => item.label.toLowerCase().includes(q) || item.group.toLowerCase().includes(q));
109
108
  },
110
109
  render: () => {
111
110
  let popup;
@@ -167,7 +166,7 @@ export const SlashCommand = Extension.create({
167
166
  return false;
168
167
  },
169
168
  onExit: () => {
170
- popup?.destroy();
169
+ // popup?.destroy();
171
170
  if (component)
172
171
  unmount(component);
173
172
  container = undefined;
@@ -0,0 +1,7 @@
1
+ import type { SuperForm } from 'sveltekit-superforms';
2
+ /**
3
+ * Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
4
+ * Only implements the 4 stores that formFieldProxy and formsnap actually use:
5
+ * form, errors, constraints, tainted.
6
+ */
7
+ export declare function createStandaloneForm(data: Record<string, unknown>): SuperForm<Record<string, unknown>>;
@@ -0,0 +1,31 @@
1
+ import { writable, readable } from 'svelte/store';
2
+ /**
3
+ * Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
4
+ * Only implements the 4 stores that formFieldProxy and formsnap actually use:
5
+ * form, errors, constraints, tainted.
6
+ */
7
+ export function createStandaloneForm(data) {
8
+ return {
9
+ form: writable(data),
10
+ errors: writable({}),
11
+ constraints: writable({}),
12
+ tainted: writable(undefined),
13
+ // Stubs required by SuperForm type — not used by formFieldProxy/formsnap
14
+ formId: writable('standalone'),
15
+ message: writable(undefined),
16
+ submitting: readable(false),
17
+ delayed: readable(false),
18
+ timeout: readable(false),
19
+ posted: readable(false),
20
+ allErrors: readable([]),
21
+ options: {},
22
+ enhance: (() => ({ destroy() { } })),
23
+ validate: (async () => undefined),
24
+ validateForm: (async () => ({ valid: true })),
25
+ isTainted: (() => false),
26
+ reset: (() => { }),
27
+ submit: (() => { }),
28
+ capture: (() => ({})),
29
+ restore: (() => { })
30
+ };
31
+ }
@@ -3,6 +3,7 @@
3
3
  import { Editor, type Editor as EditorType } from '@tiptap/core';
4
4
  import BubbleMenu from '@tiptap/extension-bubble-menu';
5
5
  import { extensions } from './extensions.js';
6
+ import Placeholder from '@tiptap/extension-placeholder';
6
7
  import ToolbarButton from './toolbar-button.svelte';
7
8
  import EditorToolbar from './editor-toolbar.svelte';
8
9
  import LinkDialog from './link-dialog.svelte';
@@ -55,6 +56,10 @@
55
56
  ...extensions,
56
57
  BubbleMenu.configure({
57
58
  element: bubbleMenu!
59
+ }),
60
+ Placeholder.configure({
61
+ placeholder: 'Wpisz treść...',
62
+ showOnlyCurrent: true
58
63
  })
59
64
  ],
60
65
  content: value || '',
@@ -246,6 +251,8 @@
246
251
 
247
252
  <style>
248
253
  .bubble-menu {
254
+ height: 0;
255
+ overflow: visible;
249
256
  visibility: hidden;
250
257
  opacity: 0;
251
258
  transition:
@@ -6,7 +6,7 @@ export declare const getRawEntries: import("@sveltejs/kit").RemoteQueryFunction<
6
6
  limit?: number | undefined;
7
7
  offset?: number | undefined;
8
8
  orderBy?: {
9
- column: "createdAt" | "updatedAt";
9
+ column: "createdAt" | "updatedAt" | "sortOrder";
10
10
  direction: "asc" | "desc";
11
11
  } | undefined;
12
12
  }, {
@@ -20,6 +20,10 @@ export declare const getEntries: import("@sveltejs/kit").RemoteQueryFunction<{
20
20
  language?: string | undefined;
21
21
  status?: "draft" | "published" | "scheduled" | "archived" | undefined;
22
22
  slug?: string | undefined;
23
+ orderBy?: {
24
+ column: "createdAt" | "updatedAt" | "sortOrder";
25
+ direction: "asc" | "desc";
26
+ } | undefined;
23
27
  }, import("../../types/entries.js").Entry[]>;
24
28
  export declare const getEntry: import("@sveltejs/kit").RemoteQueryFunction<{
25
29
  id?: string | undefined;
@@ -53,11 +57,15 @@ export declare const updateEntryCommand: import("@sveltejs/kit").RemoteCommand<{
53
57
  publishedAt?: Date | null | undefined;
54
58
  publishedVersionId?: string | null | undefined;
55
59
  publishedBy?: string | null | undefined;
60
+ sortOrder?: number | null | undefined;
56
61
  };
57
62
  }, Promise<import("../../types/entries.js").DbEntry>>;
58
63
  export declare const archiveEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<import("../../types/entries.js").DbEntry>>;
59
64
  export declare const unarchiveEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<import("../../types/entries.js").DbEntry>>;
60
65
  export declare const deleteEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
66
+ export declare const reorderEntriesCommand: import("@sveltejs/kit").RemoteCommand<{
67
+ orderedIds: string[];
68
+ }, Promise<void>>;
61
69
  export declare const getEntryVersion: import("@sveltejs/kit").RemoteQueryFunction<{
62
70
  id: string;
63
71
  language: string;
@@ -14,7 +14,7 @@ export const getRawEntries = query(z.object({
14
14
  offset: z.number().int().nonnegative().optional(),
15
15
  orderBy: z
16
16
  .object({
17
- column: z.enum(['createdAt', 'updatedAt']),
17
+ column: z.enum(['createdAt', 'updatedAt', 'sortOrder']),
18
18
  direction: z.enum(['asc', 'desc'])
19
19
  })
20
20
  .optional()
@@ -32,7 +32,13 @@ export const getEntries = query(z.object({
32
32
  dataLike: z.record(z.string(), z.unknown()).optional(),
33
33
  language: z.string().optional(),
34
34
  status: z.enum(entryStatuses).optional(),
35
- slug: z.string().optional()
35
+ slug: z.string().optional(),
36
+ orderBy: z
37
+ .object({
38
+ column: z.enum(['createdAt', 'updatedAt', 'sortOrder']),
39
+ direction: z.enum(['asc', 'desc'])
40
+ })
41
+ .optional()
36
42
  }), async (input) => {
37
43
  return getEntriesOperation(input);
38
44
  });
@@ -172,6 +178,12 @@ export const deleteEntryCommand = command(z.string().uuid(), async (id) => {
172
178
  }
173
179
  return getCMS().databaseAdapter.deleteEntry({ id });
174
180
  });
181
+ export const reorderEntriesCommand = command(z.object({
182
+ orderedIds: z.array(z.string().uuid())
183
+ }), async ({ orderedIds }) => {
184
+ requireAuth();
185
+ await Promise.all(orderedIds.map((id, index) => updateEntry(id, { sortOrder: index })));
186
+ });
175
187
  export const getEntryVersion = query(z.object({
176
188
  id: z.string().uuid(),
177
189
  language: z.string()
@@ -199,6 +211,14 @@ export const getRecentEntries = query(z.number().default(6), async (limit) => {
199
211
  label = String(Object.values(titleData)[0] || '');
200
212
  }
201
213
  }
214
+ if (config && config.type === 'single' && config.label) {
215
+ if (typeof config.label === 'object') {
216
+ label = String(Object.values(config.label)[0] || '');
217
+ }
218
+ else {
219
+ label = config.label;
220
+ }
221
+ }
202
222
  let collectionLabel = { pl: entry.slug, en: entry.slug };
203
223
  if (config) {
204
224
  if (config.type === 'collection' && config.labels) {
@@ -237,6 +257,14 @@ export const getRecentActivity = query(z.number().default(10), async (limit) =>
237
257
  label = String(Object.values(titleData)[0] || '');
238
258
  }
239
259
  }
260
+ if (config && config.type === 'single' && config.label) {
261
+ if (typeof config.label === 'object') {
262
+ label = String(Object.values(config.label)[0] || '');
263
+ }
264
+ else {
265
+ label = config.label;
266
+ }
267
+ }
240
268
  activity.push({
241
269
  type: 'entry_edit',
242
270
  entryId: entry.id,
@@ -308,6 +308,16 @@ div[data-collapsible]:not([data-collapsible='icon'])
308
308
  flex-shrink: 0;
309
309
  }
310
310
 
311
+ /* Placeholder — Notion-style */
312
+ .ProseMirror .is-empty.is-editor-empty::before,
313
+ .ProseMirror .has-focus.is-empty::before {
314
+ color: var(--text-light);
315
+ content: attr(data-placeholder);
316
+ float: left;
317
+ height: 0;
318
+ pointer-events: none;
319
+ }
320
+
311
321
  .tiptap-highlight {
312
322
  background-color: rgb(250 204 21 / 0.4);
313
323
  padding: 0.125em 0.25em;
@@ -0,0 +1,6 @@
1
+ import type { FieldCondition } from '../../types/fields.js';
2
+ /**
3
+ * Evaluate whether a field should be visible based on its showWhen condition.
4
+ * Returns true (visible) when no condition is set.
5
+ */
6
+ export declare function evaluateCondition(condition: FieldCondition | undefined, getValue: (slug: string) => unknown): boolean;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Evaluate whether a field should be visible based on its showWhen condition.
3
+ * Returns true (visible) when no condition is set.
4
+ */
5
+ export function evaluateCondition(condition, getValue) {
6
+ if (!condition)
7
+ return true;
8
+ const value = getValue(condition.field);
9
+ if (condition.equals !== undefined) {
10
+ const allowed = Array.isArray(condition.equals) ? condition.equals : [condition.equals];
11
+ return allowed.includes(value);
12
+ }
13
+ if (condition.notEquals !== undefined) {
14
+ const disallowed = Array.isArray(condition.notEquals)
15
+ ? condition.notEquals
16
+ : [condition.notEquals];
17
+ return !disallowed.includes(value);
18
+ }
19
+ return true;
20
+ }
@@ -156,6 +156,14 @@ export * from 'includio-cms/admin/remote';
156
156
  import { createAdminApiHandler } from 'includio-cms/admin/api/handler';
157
157
 
158
158
  export const { GET, POST, PATCH, PUT, DELETE } = createAdminApiHandler();
159
+ `
160
+ },
161
+ {
162
+ path: 'admin/api/rest/[...restPath]/+server.ts',
163
+ content: `${GENERATED_COMMENT_TS}
164
+ import { createRestApiHandler } from 'includio-cms/admin/api/rest/handler';
165
+
166
+ export const { GET, POST, PUT, DELETE } = createRestApiHandler();
159
167
  `
160
168
  }
161
169
  ];
@@ -0,0 +1,2 @@
1
+ import Root from "./switch.svelte";
2
+ export { Root, Root as Switch, };