includio-cms 0.5.2 → 0.5.3

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 (93) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/ROADMAP.md +13 -0
  3. package/dist/admin/client/entry/entry-form.svelte +1 -0
  4. package/dist/admin/client/entry/entry.svelte +130 -123
  5. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  6. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  7. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  8. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  9. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  10. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  11. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  12. package/dist/admin/components/fields/content-field.svelte +4 -17
  13. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  14. package/dist/admin/components/fields/date-field.svelte +8 -21
  15. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  16. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  17. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  18. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  19. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  20. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  21. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  22. package/dist/admin/components/fields/fields-form.svelte +13 -10
  23. package/dist/admin/components/fields/file-field.svelte +12 -27
  24. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  25. package/dist/admin/components/fields/image-field.svelte +13 -28
  26. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  27. package/dist/admin/components/fields/media-field.svelte +15 -30
  28. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  29. package/dist/admin/components/fields/number-field.svelte +6 -20
  30. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  31. package/dist/admin/components/fields/object-field.svelte +26 -29
  32. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  33. package/dist/admin/components/fields/radio-field.svelte +8 -20
  34. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  35. package/dist/admin/components/fields/relation-field.svelte +15 -30
  36. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  37. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  38. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  39. package/dist/admin/components/fields/select-field.svelte +14 -28
  40. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  41. package/dist/admin/components/fields/seo-field.svelte +5 -12
  42. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  43. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  44. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/slug-field.svelte +6 -11
  46. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  47. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  48. package/dist/admin/components/fields/text-field.svelte +7 -19
  49. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  50. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  51. package/dist/admin/components/fields/url-field.svelte +294 -128
  52. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  54. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  55. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  56. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  57. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  58. package/dist/admin/components/tiptap/slash-command.js +2 -3
  59. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  60. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  61. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  62. package/dist/admin/remote/entry.remote.js +16 -0
  63. package/dist/admin/styles/admin.css +10 -0
  64. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  65. package/dist/admin/utils/fieldCondition.js +20 -0
  66. package/dist/components/ui/switch/index.d.ts +2 -0
  67. package/dist/components/ui/switch/index.js +4 -0
  68. package/dist/components/ui/switch/switch.svelte +26 -0
  69. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  70. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  71. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  72. package/dist/core/fields/urlUtils.d.ts +14 -0
  73. package/dist/core/fields/urlUtils.js +21 -0
  74. package/dist/core/server/fields/populateEntry.js +43 -0
  75. package/dist/core/server/fields/resolveImageFields.js +33 -1
  76. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  77. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  78. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  79. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  80. package/dist/core/server/generator/formFields.js +2 -0
  81. package/dist/core/server/generator/generator.js +25 -1
  82. package/dist/schemas/field/url.d.ts +2 -0
  83. package/dist/schemas/field/url.js +4 -2
  84. package/dist/types/fields.d.ts +9 -0
  85. package/dist/types/formFields.d.ts +15 -2
  86. package/dist/types/index.d.ts +1 -0
  87. package/dist/types/index.js +1 -0
  88. package/dist/updates/0.5.3/index.d.ts +2 -0
  89. package/dist/updates/0.5.3/index.js +19 -0
  90. package/dist/updates/index.js +2 -1
  91. package/package.json +2 -1
  92. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  93. 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:
@@ -199,6 +199,14 @@ export const getRecentEntries = query(z.number().default(6), async (limit) => {
199
199
  label = String(Object.values(titleData)[0] || '');
200
200
  }
201
201
  }
202
+ if (config && config.type === 'single' && config.label) {
203
+ if (typeof config.label === 'object') {
204
+ label = String(Object.values(config.label)[0] || '');
205
+ }
206
+ else {
207
+ label = config.label;
208
+ }
209
+ }
202
210
  let collectionLabel = { pl: entry.slug, en: entry.slug };
203
211
  if (config) {
204
212
  if (config.type === 'collection' && config.labels) {
@@ -237,6 +245,14 @@ export const getRecentActivity = query(z.number().default(10), async (limit) =>
237
245
  label = String(Object.values(titleData)[0] || '');
238
246
  }
239
247
  }
248
+ if (config && config.type === 'single' && config.label) {
249
+ if (typeof config.label === 'object') {
250
+ label = String(Object.values(config.label)[0] || '');
251
+ }
252
+ else {
253
+ label = config.label;
254
+ }
255
+ }
240
256
  activity.push({
241
257
  type: 'entry_edit',
242
258
  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
+ }
@@ -0,0 +1,2 @@
1
+ import Root from "./switch.svelte";
2
+ export { Root, Root as Switch, };
@@ -0,0 +1,4 @@
1
+ import Root from "./switch.svelte";
2
+ export { Root,
3
+ //
4
+ Root as Switch, };
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { Switch as SwitchPrimitive } from "bits-ui";
3
+ import { cn, type WithoutChildrenOrChild } from "../../../utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ checked = $bindable(false),
8
+ class: className,
9
+ ...restProps
10
+ }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
11
+ </script>
12
+
13
+ <SwitchPrimitive.Root
14
+ bind:ref
15
+ bind:checked
16
+ data-slot="switch"
17
+ class={cn(
18
+ "peer inline-flex h-[22px] w-[38px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-border",
19
+ className
20
+ )}
21
+ {...restProps}
22
+ >
23
+ <SwitchPrimitive.Thumb
24
+ class="pointer-events-none block size-[18px] rounded-full bg-white shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
25
+ />
26
+ </SwitchPrimitive.Root>
@@ -0,0 +1,4 @@
1
+ import { Switch as SwitchPrimitive } from "bits-ui";
2
+ declare const Switch: import("svelte").Component<Omit<Omit<SwitchPrimitive.RootProps, "child">, "children">, {}, "ref" | "checked">;
3
+ type Switch = ReturnType<typeof Switch>;
4
+ export default Switch;
@@ -139,7 +139,8 @@ export function generateZodSchemaFromField(field, languages, options = {
139
139
  }
140
140
  case 'object': {
141
141
  const data = generateZodSchemaFromFields(field.fields, languages, {
142
- parentRequired: field.required
142
+ parentRequired: field.required,
143
+ localized: options.localized
143
144
  });
144
145
  const finalData = !field.required ? data.partial().optional() : data;
145
146
  return z.object({
@@ -148,7 +149,10 @@ export function generateZodSchemaFromField(field, languages, options = {
148
149
  });
149
150
  }
150
151
  case 'blocks': {
151
- const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, { parentRequired: field.required }));
152
+ const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, {
153
+ parentRequired: field.required,
154
+ localized: options.localized
155
+ }));
152
156
  const itemSchema = schemas.length > 1
153
157
  ? z.discriminatedUnion('slug', schemas)
154
158
  : schemas[0];
@@ -283,6 +287,14 @@ export function generateZodSchemaFromField(field, languages, options = {
283
287
  }
284
288
  export function generateZodSchemaFromFields(fields, languages, options = {}) {
285
289
  return z.object({
286
- ...Object.fromEntries(fields.map((f) => [f.slug, generateZodSchemaFromField(f, languages, options)]))
290
+ ...Object.fromEntries(fields.map((f) => {
291
+ let schema = generateZodSchemaFromField(f, languages, options);
292
+ // Fields with showWhen are conditionally visible — make them optional
293
+ // so hidden fields don't block validation
294
+ if (f.showWhen) {
295
+ schema = schema.optional();
296
+ }
297
+ return [f.slug, schema];
298
+ }))
287
299
  });
288
300
  }