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,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getRawCollectionEntryLabel } from '../../utils/entryLabel.js';
3
3
  import { getEntryThumbnail } from '../../utils/entryThumbnail.js';
4
+ import { arrayMove } from '../../utils/arrayMove.js';
4
5
  import { getRemotes } from '../../../sveltekit/index.js';
5
6
  import type { CollectionConfigWithType } from '../../../types/collections.js';
6
7
  import DataTable from './data-table.svelte';
@@ -11,6 +12,7 @@
11
12
  import type { InterfaceLanguage } from '../../../types/languages.js';
12
13
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
13
14
  import { getEntryStatus } from '../entry/utils.js';
15
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
14
16
  import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
15
17
  import { toast } from 'svelte-sonner';
16
18
  import StatusBadge from './status-badge.svelte';
@@ -110,6 +112,7 @@
110
112
  thumbnail: string | null;
111
113
  searchText: string;
112
114
  a11yWarnings: number;
115
+ customData: Record<string, unknown>;
113
116
  }
114
117
 
115
118
  type Props = {
@@ -140,8 +143,14 @@
140
143
  // Is the current filter for archived entries?
141
144
  const isArchivedFilter = $derived(viewState.statusFilter === 'archived');
142
145
 
146
+ const isOrderable = $derived(!!collection.orderable);
147
+
143
148
  // Build orderBy from sorting state for server-side mode
144
149
  const serverOrderBy = $derived.by(() => {
150
+ // When orderable, always sort by sortOrder
151
+ if (isOrderable) {
152
+ return { column: 'sortOrder' as const, direction: 'asc' as const };
153
+ }
145
154
  if (!viewState.sorting.length) return undefined;
146
155
  const { id, desc } = viewState.sorting[0];
147
156
  if (id === 'createdAt' || id === 'updatedAt') {
@@ -240,6 +249,23 @@
240
249
  }),
241
250
  size: 140
242
251
  },
252
+ // Custom list columns from collection config
253
+ ...(collection.listColumns ?? []).map((fieldSlug) => {
254
+ const fieldConfig = collection.fields.find((f) => f.slug === fieldSlug);
255
+ const headerLabel = fieldConfig?.label
256
+ ? getLocalizedLabel(fieldConfig.label, interfaceLanguage.current)
257
+ : fieldSlug;
258
+ return {
259
+ id: `custom_${fieldSlug}`,
260
+ header: headerLabel,
261
+ cell: (info: { row: { original: CollectionDataTableRow } }) => {
262
+ const value = info.row.original.customData[fieldSlug];
263
+ return String(value ?? '');
264
+ },
265
+ enableSorting: false,
266
+ size: 120
267
+ } satisfies ColumnDef<CollectionDataTableRow>;
268
+ }),
243
269
  {
244
270
  id: 'actions',
245
271
  header: '',
@@ -331,6 +357,19 @@
331
357
 
332
358
  function mapEntryToRow(entry: RawEntry): CollectionDataTableRow {
333
359
  const data = entry.draftVersion?.data || entry.publishedVersion?.data || {};
360
+ const customData: Record<string, unknown> = {};
361
+ if (collection.listColumns) {
362
+ for (const fieldSlug of collection.listColumns) {
363
+ const fieldData = (data as Record<string, unknown>)[fieldSlug];
364
+ // For localized fields, extract current content language value
365
+ if (fieldData && typeof fieldData === 'object' && !Array.isArray(fieldData)) {
366
+ const localized = fieldData as Record<string, unknown>;
367
+ customData[fieldSlug] = localized[contentLanguage.current] ?? Object.values(localized)[0] ?? '';
368
+ } else {
369
+ customData[fieldSlug] = fieldData ?? '';
370
+ }
371
+ }
372
+ }
334
373
  return {
335
374
  id: entry.id,
336
375
  name: getRawCollectionEntryLabel(entry, collection, contentLanguage.current),
@@ -341,7 +380,8 @@
341
380
  updatedAt: new Date(entry.updatedAt),
342
381
  thumbnail: getEntryThumbnail(data as Record<string, unknown>, collection),
343
382
  searchText: getSearchText(entry),
344
- a11yWarnings: countA11yWarnings(entry)
383
+ a11yWarnings: countA11yWarnings(entry),
384
+ customData
345
385
  };
346
386
  }
347
387
 
@@ -404,6 +444,14 @@
404
444
  refreshQueries();
405
445
  }
406
446
 
447
+ async function handleReorder(items: CollectionDataTableRow[], fromIndex: number, toIndex: number) {
448
+ if (!isOrderable || items.length === 0) return;
449
+ const reordered = arrayMove(items, fromIndex, toIndex);
450
+ const orderedIds = reordered.map((item) => item.id);
451
+ await remotes.reorderEntriesCommand({ orderedIds });
452
+ refreshQueries();
453
+ }
454
+
407
455
  async function handleBulkDelete(items: CollectionDataTableRow[]) {
408
456
  const idsToDelete = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
409
457
  for (const id of idsToDelete) {
@@ -439,8 +487,7 @@
439
487
  ? result.total
440
488
  : filteredRows.length}
441
489
  {@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
442
- {@const displayItems =
443
- useServerPagination && !viewState.statusFilter
490
+ {@const displayItems = useServerPagination && !viewState.statusFilter
444
491
  ? filteredRows
445
492
  : filteredRows.slice(
446
493
  viewState.pageIndex * viewState.pageSize,
@@ -474,7 +521,7 @@
474
521
  <DataTable
475
522
  data={displayItems}
476
523
  {columns}
477
- enableSorting
524
+ enableSorting={!isOrderable}
478
525
  enableFiltering
479
526
  enableSelection
480
527
  enablePagination
@@ -490,12 +537,14 @@
490
537
  viewState.pageIndex = p.pageIndex;
491
538
  }}
492
539
  tableRef={(t) => (tableInstance = t)}
540
+ orderable={isOrderable}
541
+ onReorder={(from, to) => handleReorder(displayItems, from, to)}
493
542
  />
494
543
  {:else}
495
544
  <DataTable
496
545
  data={displayItems}
497
546
  {columns}
498
- enableSorting
547
+ enableSorting={!isOrderable}
499
548
  enableFiltering
500
549
  enableSelection
501
550
  enablePagination
@@ -508,6 +557,8 @@
508
557
  viewState.pageIndex = p.pageIndex;
509
558
  }}
510
559
  tableRef={(t) => (tableInstance = t)}
560
+ orderable={isOrderable}
561
+ onReorder={(from, to) => handleReorder(displayItems, from, to)}
511
562
  />
512
563
  {/if}
513
564
  </div>
@@ -15,6 +15,8 @@
15
15
  import * as Table from '../../../components/ui/table/index.js';
16
16
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
17
17
  import type { InterfaceLanguage } from '../../../types/languages.js';
18
+ import { droppable, draggable, dndState } from '@thisux/sveltednd';
19
+ import { flip } from 'svelte/animate';
18
20
 
19
21
  type DataTableProps<TData, TValue> = {
20
22
  columns: ColumnDef<TData, TValue>[];
@@ -35,6 +37,8 @@
35
37
  pageCount?: number;
36
38
  rowCount?: number;
37
39
  emptyMessage?: string;
40
+ orderable?: boolean;
41
+ onReorder?: (fromIndex: number, toIndex: number) => void;
38
42
  };
39
43
 
40
44
  let {
@@ -55,9 +59,18 @@
55
59
  manualPagination: manualPaginationProp = false,
56
60
  pageCount: pageCountProp,
57
61
  rowCount: rowCountProp,
58
- emptyMessage
62
+ emptyMessage,
63
+ orderable = false,
64
+ onReorder
59
65
  }: DataTableProps<TData, TValue> = $props();
60
66
 
67
+ let liveRegionMessage = $state('');
68
+ let dropProcessing = false;
69
+
70
+ const matchesReducedMotion = typeof window !== 'undefined'
71
+ ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
72
+ : false;
73
+
61
74
  const interfaceLanguage = useInterfaceLanguage();
62
75
 
63
76
  const defaultEmptyMessages: Record<InterfaceLanguage, string> = {
@@ -113,10 +126,17 @@
113
126
  });
114
127
  </script>
115
128
 
116
- <Table.Root>
129
+ {#if orderable}
130
+ <div aria-live="polite" aria-atomic="true" class="sr-only">{liveRegionMessage}</div>
131
+ {/if}
132
+
133
+ <Table.Root aria-roledescription={orderable ? 'sortable list' : undefined}>
117
134
  <Table.Header class="bg-muted sticky top-0 z-10">
118
135
  {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
119
136
  <Table.Row class="border-b-0 hover:bg-transparent">
137
+ {#if orderable}
138
+ <Table.Head class="text-[11px] font-bold uppercase tracking-[0.04em] text-muted-foreground h-10 w-10"></Table.Head>
139
+ {/if}
120
140
  {#each headerGroup.headers as header (header.id)}
121
141
  <Table.Head class="text-[11px] font-bold uppercase tracking-[0.04em] text-muted-foreground h-10">
122
142
  {#if !header.isPlaceholder}
@@ -131,23 +151,112 @@
131
151
  {/each}
132
152
  </Table.Header>
133
153
  <Table.Body>
134
- {#each table.getRowModel().rows as row, i (row.id)}
135
- <Table.Row
136
- data-state={row.getIsSelected() && 'selected'}
137
- class="hover:bg-lavender-lighter/50 data-[state=selected]:bg-lavender-lighter/30 {i === table.getRowModel().rows.length - 1 ? 'border-b-0' : ''}"
138
- >
139
- {#each row.getVisibleCells() as cell (cell.id)}
140
- <Table.Cell class="py-3">
141
- <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
154
+ {#if orderable}
155
+ {#each table.getRowModel().rows as row, i (row.id)}
156
+ {@const totalRows = table.getRowModel().rows.length}
157
+ <tr
158
+ use:droppable={{
159
+ container: i.toString(),
160
+ callbacks: {
161
+ onDrop: (state) => {
162
+ if (dropProcessing) return;
163
+ dropProcessing = true;
164
+ const fromIndex = parseInt(state.sourceContainer ?? '');
165
+ const toIndex = parseInt(state.targetContainer ?? '');
166
+ if (!isNaN(fromIndex) && !isNaN(toIndex)) {
167
+ onReorder?.(fromIndex, toIndex);
168
+ }
169
+ dndState.isDragging = false;
170
+ dndState.draggedItem = null;
171
+ dndState.sourceContainer = '';
172
+ dndState.targetContainer = null;
173
+ dndState.targetElement = null;
174
+ setTimeout(() => { dropProcessing = false; }, 50);
175
+ }
176
+ }
177
+ }}
178
+ animate:flip={{ duration: matchesReducedMotion ? 0 : 200 }}
179
+ data-slot="table-row"
180
+ data-state={row.getIsSelected() ? 'selected' : undefined}
181
+ class="hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors hover:bg-lavender-lighter/50 data-[state=selected]:bg-lavender-lighter/30 {i === totalRows - 1 ? 'border-b-0' : ''}"
182
+ >
183
+ <Table.Cell class="py-3 w-10">
184
+ <div class="flex items-center gap-1">
185
+ <div
186
+ use:draggable={{
187
+ container: i.toString(),
188
+ dragData: { index: i }
189
+ }}
190
+ class="text-muted-foreground hover:text-foreground cursor-grab"
191
+ role="button"
192
+ tabindex="0"
193
+ aria-label={`Drag to reorder, row ${i + 1} of ${totalRows}`}
194
+ aria-roledescription="draggable"
195
+ >
196
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>
197
+ </div>
198
+ <div class="flex flex-col">
199
+ {#if i > 0}
200
+ <button
201
+ type="button"
202
+ class="p-0.5 rounded text-muted-foreground hover:text-foreground"
203
+ aria-label={`Move up, row ${i + 1} of ${totalRows}`}
204
+ onclick={() => {
205
+ onReorder?.(i, i - 1);
206
+ liveRegionMessage = `Moved to position ${i} of ${totalRows}`;
207
+ }}
208
+ >
209
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg>
210
+ </button>
211
+ {/if}
212
+ {#if i < totalRows - 1}
213
+ <button
214
+ type="button"
215
+ class="p-0.5 rounded text-muted-foreground hover:text-foreground"
216
+ aria-label={`Move down, row ${i + 1} of ${totalRows}`}
217
+ onclick={() => {
218
+ onReorder?.(i, i + 1);
219
+ liveRegionMessage = `Moved to position ${i + 2} of ${totalRows}`;
220
+ }}
221
+ >
222
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
223
+ </button>
224
+ {/if}
225
+ </div>
226
+ </div>
142
227
  </Table.Cell>
143
- {/each}
144
- </Table.Row>
228
+ {#each row.getVisibleCells() as cell (cell.id)}
229
+ <Table.Cell class="py-3">
230
+ <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
231
+ </Table.Cell>
232
+ {/each}
233
+ </tr>
234
+ {:else}
235
+ <Table.Row>
236
+ <Table.Cell colspan={columns.length + 1} class="h-24 text-center text-muted-foreground">
237
+ {emptyMessage ?? defaultEmptyMessages[interfaceLanguage.current]}
238
+ </Table.Cell>
239
+ </Table.Row>
240
+ {/each}
145
241
  {:else}
146
- <Table.Row>
147
- <Table.Cell colspan={columns.length} class="h-24 text-center text-muted-foreground">
148
- {emptyMessage ?? defaultEmptyMessages[interfaceLanguage.current]}
149
- </Table.Cell>
150
- </Table.Row>
151
- {/each}
242
+ {#each table.getRowModel().rows as row, i (row.id)}
243
+ <Table.Row
244
+ data-state={row.getIsSelected() && 'selected'}
245
+ class="hover:bg-lavender-lighter/50 data-[state=selected]:bg-lavender-lighter/30 {i === table.getRowModel().rows.length - 1 ? 'border-b-0' : ''}"
246
+ >
247
+ {#each row.getVisibleCells() as cell (cell.id)}
248
+ <Table.Cell class="py-3">
249
+ <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
250
+ </Table.Cell>
251
+ {/each}
252
+ </Table.Row>
253
+ {:else}
254
+ <Table.Row>
255
+ <Table.Cell colspan={columns.length} class="h-24 text-center text-muted-foreground">
256
+ {emptyMessage ?? defaultEmptyMessages[interfaceLanguage.current]}
257
+ </Table.Cell>
258
+ </Table.Row>
259
+ {/each}
260
+ {/if}
152
261
  </Table.Body>
153
262
  </Table.Root>
@@ -18,6 +18,8 @@ type DataTableProps<TData, TValue> = {
18
18
  pageCount?: number;
19
19
  rowCount?: number;
20
20
  emptyMessage?: string;
21
+ orderable?: boolean;
22
+ onReorder?: (fromIndex: number, toIndex: number) => void;
21
23
  };
22
24
  declare function $$render<TData, TValue>(): {
23
25
  props: DataTableProps<TData, TValue>;
@@ -112,6 +112,7 @@
112
112
 
113
113
  <style>
114
114
  .layout-entry-form {
115
+ container-type: inline-size;
115
116
  max-width: 1100px;
116
117
  margin: 0 auto;
117
118
  padding: 24px;
@@ -13,6 +13,7 @@
13
13
  import ArchiveIcon from '@tabler/icons-svelte/icons/archive';
14
14
  import XIcon from '@tabler/icons-svelte/icons/x';
15
15
  import Button from '../../../components/ui/button/button.svelte';
16
+ import { cn } from '../../../utils.js';
16
17
  import { ElementSize, useDebounce } from 'runed';
17
18
  import { defaults, superForm, type SuperForm } from 'sveltekit-superforms';
18
19
  import { zod4, zod4Client } from 'sveltekit-superforms/adapters';
@@ -326,9 +327,7 @@
326
327
  validationErrors = errors;
327
328
 
328
329
  // Scroll to first errored field
329
- const firstErrorKey = Object.keys(validatedForm.errors).find(
330
- (k) => k !== '_errors'
331
- );
330
+ const firstErrorKey = Object.keys(validatedForm.errors).find((k) => k !== '_errors');
332
331
  if (firstErrorKey) {
333
332
  scrollToIssue(firstErrorKey);
334
333
  }
@@ -540,137 +539,145 @@
540
539
  });
541
540
 
542
541
  const t = $derived(lang[interfaceLanguage.current]);
542
+ const isHybrid = $derived(hybridContext.mode === 'hybrid' && !!collection.previewUrl);
543
543
  </script>
544
544
 
545
- <EntryHeader
546
- {entry}
547
- version={editingEntry}
548
- {onSave}
549
- onSaveDraft={performAutosave}
550
- {onArchive}
551
- {saveStatus}
552
- {isArchived}
553
- fields={getFieldsFromConfig(collection)}
554
- getFormData={() => get(form.form)}
555
- onScrollToIssue={scrollToIssue}
556
- {translationStatus}
557
- />
558
-
559
- {#if validationErrors.length > 0}
560
- <div
561
- role="alert"
562
- aria-live="assertive"
563
- class="flex items-start gap-3 border-b border-[var(--error)]/20 bg-[#FDF0F0] px-6 py-3 text-sm text-[var(--error)]"
564
- >
565
- <AlertCircle class="mt-0.5 size-4 shrink-0" />
566
- <div class="min-w-0 flex-1">
567
- <p class="font-semibold">{t.cannotPublish}</p>
568
- <p class="text-xs opacity-80">{t.validationHint}</p>
569
- <ul class="mt-1 text-xs">
570
- {#each validationErrors.slice(0, 5) as error}
571
- <li>— {error}</li>
572
- {/each}
573
- </ul>
574
- </div>
575
- <button
576
- type="button"
577
- onclick={() => (validationErrors = [])}
578
- class="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100"
579
- aria-label="Zamknij"
545
+ <div class={isHybrid ? 'flex h-full flex-col overflow-hidden' : ''}>
546
+ <EntryHeader
547
+ {entry}
548
+ version={editingEntry}
549
+ {onSave}
550
+ onSaveDraft={performAutosave}
551
+ {onArchive}
552
+ {saveStatus}
553
+ {isArchived}
554
+ fields={getFieldsFromConfig(collection)}
555
+ getFormData={() => get(form.form)}
556
+ onScrollToIssue={scrollToIssue}
557
+ {translationStatus}
558
+ />
559
+
560
+ {#if validationErrors.length > 0}
561
+ <div
562
+ role="alert"
563
+ aria-live="assertive"
564
+ class="flex shrink-0 items-start gap-3 border-b border-[var(--error)]/20 bg-[#FDF0F0] px-6 py-3 text-sm text-[var(--error)]"
580
565
  >
581
- <XIcon class="size-4" />
582
- </button>
583
- </div>
584
- {/if}
566
+ <AlertCircle class="mt-0.5 size-4 shrink-0" />
567
+ <div class="min-w-0 flex-1">
568
+ <p class="font-semibold">{t.cannotPublish}</p>
569
+ <p class="text-xs opacity-80">{t.validationHint}</p>
570
+ <ul class="mt-1 text-xs">
571
+ {#each validationErrors.slice(0, 5) as error}
572
+ <li>— {error}</li>
573
+ {/each}
574
+ </ul>
575
+ </div>
576
+ <button
577
+ type="button"
578
+ onclick={() => (validationErrors = [])}
579
+ class="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100"
580
+ aria-label="Zamknij"
581
+ >
582
+ <XIcon class="size-4" />
583
+ </button>
584
+ </div>
585
+ {/if}
585
586
 
586
- {#if isArchived}
587
- <div
588
- role="alert"
589
- class="flex items-center justify-between gap-3 border-b border-[var(--warning)]/20 bg-[#FDF6EC] px-6 py-3 text-sm"
590
- >
591
- <div class="flex items-center gap-2 text-[var(--warning)]">
592
- <ArchiveIcon class="size-4 shrink-0" />
593
- <span>{t.archivedBanner}</span>
587
+ {#if isArchived}
588
+ <div
589
+ role="alert"
590
+ class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--warning)]/20 bg-[#FDF6EC] px-6 py-3 text-sm"
591
+ >
592
+ <div class="flex items-center gap-2 text-[var(--warning)]">
593
+ <ArchiveIcon class="size-4 shrink-0" />
594
+ <span>{t.archivedBanner}</span>
595
+ </div>
596
+ <Button size="sm" onclick={onRestore}>{t.restore}</Button>
594
597
  </div>
595
- <Button size="sm" onclick={onRestore}>{t.restore}</Button>
596
- </div>
597
- {/if}
598
+ {/if}
598
599
 
599
- {#if showDraftBanner}
600
- <div
601
- class="flex items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
602
- >
603
- <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].newerDraft}</span>
604
- <button
605
- type="button"
606
- class="font-semibold text-[var(--primary)] hover:underline"
607
- onclick={() => goto(`?version=${draftVersionId}`)}
600
+ {#if showDraftBanner}
601
+ <div
602
+ class="flex shrink-0 items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
608
603
  >
609
- {lang[interfaceLanguage.current].switchToDraft}
610
- </button>
611
- </div>
612
- {:else if showPublishedBanner}
604
+ <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].newerDraft}</span>
605
+ <button
606
+ type="button"
607
+ class="font-semibold text-[var(--primary)] hover:underline"
608
+ onclick={() => goto(`?version=${draftVersionId}`)}
609
+ >
610
+ {lang[interfaceLanguage.current].switchToDraft}
611
+ </button>
612
+ </div>
613
+ {:else if showPublishedBanner}
614
+ <div
615
+ class="flex shrink-0 items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
616
+ >
617
+ <span class="text-[var(--text-secondary)]"
618
+ >{lang[interfaceLanguage.current].editingDraft}</span
619
+ >
620
+ <button
621
+ type="button"
622
+ class="font-semibold text-[var(--primary)] hover:underline"
623
+ onclick={() => goto(`?version=${entry.publishedVersion!.id}`)}
624
+ >
625
+ {lang[interfaceLanguage.current].switchToPublished}
626
+ </button>
627
+ </div>
628
+ {/if}
629
+
613
630
  <div
614
- class="flex items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
631
+ class={cn(
632
+ isArchived && 'pointer-events-none opacity-60',
633
+ isHybrid && 'flex min-h-0 flex-1 overflow-hidden'
634
+ )}
615
635
  >
616
- <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].editingDraft}</span>
617
- <button
618
- type="button"
619
- class="font-semibold text-[var(--primary)] hover:underline"
620
- onclick={() => goto(`?version=${entry.publishedVersion!.id}`)}
621
- >
622
- {lang[interfaceLanguage.current].switchToPublished}
623
- </button>
624
- </div>
625
- {/if}
626
-
627
- <div class={isArchived ? 'pointer-events-none opacity-60' : ''}>
628
- {#if hybridContext.mode === 'hybrid' && collection.previewUrl}
629
- <div class="flex min-h-0 flex-1 overflow-hidden">
630
- {#await import('./hybrid/hybrid-layout.svelte')}
631
- <div class="bg-accent h-full animate-pulse rounded-md"></div>
632
- {:then { default: HybridLayout }}
633
- <HybridLayout>
634
- {#snippet preview()}
635
- {#await import('./hybrid/hybrid-preview.svelte')}
636
- <div class="bg-accent h-full animate-pulse rounded-md"></div>
637
- {:then { default: HybridPreview }}
638
- <HybridPreview
639
- {collection}
640
- {editingEntry}
641
- bind:previewIframe
642
- bind:sizePreset
643
- {size}
644
- bind:el
636
+ {#if isHybrid}
637
+ {#await import('./hybrid/hybrid-layout.svelte')}
638
+ <div class="bg-accent h-full animate-pulse rounded-md"></div>
639
+ {:then { default: HybridLayout }}
640
+ <HybridLayout>
641
+ {#snippet preview()}
642
+ {#await import('./hybrid/hybrid-preview.svelte')}
643
+ <div class="bg-accent h-full animate-pulse rounded-md"></div>
644
+ {:then { default: HybridPreview }}
645
+ <HybridPreview
646
+ {collection}
647
+ {editingEntry}
648
+ bind:previewIframe
649
+ bind:sizePreset
650
+ {size}
651
+ bind:el
652
+ />
653
+ {:catch}
654
+ <p class="text-destructive p-4 text-sm">Failed to load preview</p>
655
+ {/await}
656
+ {/snippet}
657
+ {#snippet formPanel()}
658
+ <EntryForm
659
+ {form}
660
+ {entry}
661
+ focusedPath={hybridContext.focusedPath}
662
+ onPathSelect={(path) => (hybridContext.focusedPath = path)}
645
663
  />
646
- {:catch}
647
- <p class="text-destructive p-4 text-sm">Failed to load preview</p>
648
- {/await}
649
- {/snippet}
650
- {#snippet formPanel()}
651
- <EntryForm
652
- {form}
653
- {entry}
654
- focusedPath={hybridContext.focusedPath}
655
- onPathSelect={(path) => (hybridContext.focusedPath = path)}
656
- />
657
- {/snippet}
658
- </HybridLayout>
659
- {:catch}
660
- <p class="text-destructive p-4 text-sm">Failed to load layout</p>
661
- {/await}
662
- </div>
663
- {:else if hasLayout(collection)}
664
- <div class="overflow-y-auto" style="scroll-padding-top: 48px;">
665
- <EntryForm {form} {entry} />
666
- </div>
667
- {:else}
668
- <div class="flex items-stretch justify-center" style="scroll-padding-top: 48px;">
669
- <div class="max-w-2xl grow p-4 lg:p-6">
670
- <div class="bg-card rounded-2xl border p-4 shadow-sm lg:p-6">
664
+ {/snippet}
665
+ </HybridLayout>
666
+ {:catch}
667
+ <p class="text-destructive p-4 text-sm">Failed to load layout</p>
668
+ {/await}
669
+ {:else if hasLayout(collection)}
670
+ <div class="overflow-y-auto" style="scroll-padding-top: 48px;">
671
671
  <EntryForm {form} {entry} />
672
672
  </div>
673
- </div>
673
+ {:else}
674
+ <div class="flex items-stretch justify-center" style="scroll-padding-top: 48px;">
675
+ <div class="max-w-2xl grow p-4 lg:p-6">
676
+ <div class="bg-card rounded-2xl border p-4 shadow-sm lg:p-6">
677
+ <EntryForm {form} {entry} />
678
+ </div>
679
+ </div>
680
+ </div>
681
+ {/if}
674
682
  </div>
675
- {/if}
676
683
  </div>