@quoin-cms/admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +661 -0
  2. package/biome.json +62 -0
  3. package/dist/assets/index-C9Y5-AKj.js +33 -0
  4. package/dist/assets/index-uVdiUjty.css +1 -0
  5. package/dist/index.html +20 -0
  6. package/index.html +19 -0
  7. package/package.json +43 -0
  8. package/src/AdminRoot.svelte +98 -0
  9. package/src/app.css +211 -0
  10. package/src/lib/Slot.svelte +65 -0
  11. package/src/lib/api/auth.ts +26 -0
  12. package/src/lib/api/client.ts +73 -0
  13. package/src/lib/api/files.ts +56 -0
  14. package/src/lib/api/globals.ts +13 -0
  15. package/src/lib/api/records.ts +102 -0
  16. package/src/lib/api/schema.ts +7 -0
  17. package/src/lib/api/versions.ts +40 -0
  18. package/src/lib/components/AdminHeader.svelte +107 -0
  19. package/src/lib/components/AdminSidebar.svelte +262 -0
  20. package/src/lib/components/DeleteDialog.svelte +58 -0
  21. package/src/lib/components/DocumentEditLayout.svelte +263 -0
  22. package/src/lib/components/DynamicForm.svelte +74 -0
  23. package/src/lib/components/KpiCard.svelte +75 -0
  24. package/src/lib/components/MediaLibrary.svelte +311 -0
  25. package/src/lib/components/Pagination.svelte +78 -0
  26. package/src/lib/components/RangeFilter.svelte +41 -0
  27. package/src/lib/components/RecordGrid.svelte +123 -0
  28. package/src/lib/components/RecordTable.svelte +156 -0
  29. package/src/lib/components/cells/CheckboxCell.svelte +10 -0
  30. package/src/lib/components/cells/ColorCell.svelte +15 -0
  31. package/src/lib/components/cells/DateCell.svelte +8 -0
  32. package/src/lib/components/cells/RelationshipCell.svelte +20 -0
  33. package/src/lib/components/cells/RichTextCell.svelte +21 -0
  34. package/src/lib/components/cells/SelectCell.svelte +26 -0
  35. package/src/lib/components/cells/TextCell.svelte +8 -0
  36. package/src/lib/components/cells/UploadCell.svelte +34 -0
  37. package/src/lib/components/cells/index.ts +28 -0
  38. package/src/lib/components/charts/TimeSeriesChart.svelte +184 -0
  39. package/src/lib/components/doc/ApiView.svelte +181 -0
  40. package/src/lib/components/doc/Autosave.svelte +102 -0
  41. package/src/lib/components/doc/DocHeader.svelte +86 -0
  42. package/src/lib/components/doc/DocMetaStrip.svelte +103 -0
  43. package/src/lib/components/doc/DocTabBar.svelte +26 -0
  44. package/src/lib/components/doc/HeaderModeSwitch.svelte +32 -0
  45. package/src/lib/components/doc/PublishButton.svelte +114 -0
  46. package/src/lib/components/doc/ScheduleModal.svelte +110 -0
  47. package/src/lib/components/doc/VersionHistory.svelte +20 -0
  48. package/src/lib/components/fields/ArrayFieldEditor.svelte +62 -0
  49. package/src/lib/components/fields/BlockCard.svelte +63 -0
  50. package/src/lib/components/fields/BlocksFieldEditor.svelte +83 -0
  51. package/src/lib/components/fields/CheckboxField.svelte +27 -0
  52. package/src/lib/components/fields/ColorField.svelte +46 -0
  53. package/src/lib/components/fields/DateField.svelte +52 -0
  54. package/src/lib/components/fields/EmailField.svelte +30 -0
  55. package/src/lib/components/fields/FileField.svelte +280 -0
  56. package/src/lib/components/fields/JsonField.svelte +145 -0
  57. package/src/lib/components/fields/NumberField.svelte +44 -0
  58. package/src/lib/components/fields/PasswordField.svelte +38 -0
  59. package/src/lib/components/fields/RelationshipField.svelte +271 -0
  60. package/src/lib/components/fields/RichTextField.svelte +139 -0
  61. package/src/lib/components/fields/SelectField.svelte +33 -0
  62. package/src/lib/components/fields/SlugField.svelte +70 -0
  63. package/src/lib/components/fields/TabsField.svelte +56 -0
  64. package/src/lib/components/fields/TagsField.svelte +85 -0
  65. package/src/lib/components/fields/TextField.svelte +36 -0
  66. package/src/lib/components/fields/TextareaField.svelte +32 -0
  67. package/src/lib/components/fields/UploadField.svelte +166 -0
  68. package/src/lib/components/fields/UploadFieldDispatch.svelte +21 -0
  69. package/src/lib/components/fields/UploadGalleryField.svelte +166 -0
  70. package/src/lib/components/fields/index.ts +22 -0
  71. package/src/lib/components/fields/registry.ts +58 -0
  72. package/src/lib/components/lexical/CustomHTMLComponent.svelte +52 -0
  73. package/src/lib/components/lexical/CustomHTMLNode.ts +94 -0
  74. package/src/lib/components/lexical/PullQuoteComponent.svelte +73 -0
  75. package/src/lib/components/lexical/PullQuoteNode.ts +112 -0
  76. package/src/lib/components/lexical/lexical-helpers.ts +24 -0
  77. package/src/lib/components/lexical/nodes.ts +8 -0
  78. package/src/lib/components/lexical/toolbar/EditorToolbar.svelte +159 -0
  79. package/src/lib/components/lexical/toolbar/InsertBlockDropdown.svelte +278 -0
  80. package/src/lib/components/versions/CompareSelector.svelte +31 -0
  81. package/src/lib/components/versions/FieldDiff.svelte +141 -0
  82. package/src/lib/components/versions/RestoreModal.svelte +67 -0
  83. package/src/lib/components/versions/StatusPill.svelte +21 -0
  84. package/src/lib/context.svelte.ts +156 -0
  85. package/src/lib/router/index.svelte.ts +282 -0
  86. package/src/lib/router/matcher.ts +52 -0
  87. package/src/lib/stores/branding.svelte.ts +74 -0
  88. package/src/lib/stores/schema.svelte.ts +17 -0
  89. package/src/lib/types/schema.ts +126 -0
  90. package/src/lib/utils/cn.ts +6 -0
  91. package/src/lib/utils/diff.ts +112 -0
  92. package/src/lib/utils/dirty.svelte.ts +50 -0
  93. package/src/lib/utils/format.ts +28 -0
  94. package/src/lib/utils/json-highlight.ts +34 -0
  95. package/src/lib/utils/slug.ts +8 -0
  96. package/src/main.ts +32 -0
  97. package/src/views/AdminLayout.svelte +73 -0
  98. package/src/views/AdsAnalyticsView.svelte +152 -0
  99. package/src/views/CollectionEditView.svelte +117 -0
  100. package/src/views/CollectionListView.svelte +347 -0
  101. package/src/views/CollectionNewView.svelte +68 -0
  102. package/src/views/CustomPageView.svelte +59 -0
  103. package/src/views/DashboardView.svelte +370 -0
  104. package/src/views/GlobalEditView.svelte +100 -0
  105. package/src/views/LoginView.svelte +231 -0
  106. package/src/views/NotFoundView.svelte +9 -0
  107. package/src/views/VersionDetailView.svelte +307 -0
  108. package/src/views/VersionsListView.svelte +201 -0
  109. package/tsconfig.json +25 -0
  110. package/vite.config.ts +80 -0
@@ -0,0 +1,201 @@
1
+ <script lang="ts">
2
+ import { page } from '$lib/router/index.svelte.js'
3
+ import { resolve } from '$lib/router/index.svelte.js'
4
+ import { goto } from '$lib/router/index.svelte.js'
5
+ import { schema, getCollectionByKey } from '$lib/stores/schema.svelte.js'
6
+ import { listVersions, restoreVersion } from '$lib/api/versions.js'
7
+ import type { Version } from '$lib/api/versions.js'
8
+ import StatusPill from '$lib/components/versions/StatusPill.svelte'
9
+ import RestoreModal from '$lib/components/versions/RestoreModal.svelte'
10
+ import Pagination from '$lib/components/Pagination.svelte'
11
+ import { toast } from 'svelte-sonner'
12
+ import { RotateCcw } from 'lucide-svelte'
13
+ import { onMount } from 'svelte'
14
+
15
+ let collectionKey = $derived(page.params.collection ?? '')
16
+ let recordId = $derived(page.params.id ?? '')
17
+ let collection = $derived(getCollectionByKey(schema.collections, collectionKey))
18
+
19
+ let versions = $state<Version[]>([])
20
+ let isLoading = $state(true)
21
+ let currentPage = $state(1)
22
+ let perPage = 20
23
+ let totalRecords = $state(0)
24
+
25
+ let restoreModalOpen = $state(false)
26
+ let restoreTargetId = $state('')
27
+ let isRestoring = $state(false)
28
+
29
+ let totalPages = $derived(Math.max(1, Math.ceil(totalRecords / perPage)))
30
+
31
+ let latestPublishedId = $derived.by(() => {
32
+ const v = versions.find((v) => v._status === 'published' && v.latest)
33
+ return v?.id ?? versions.find((v) => v._status === 'published')?.id ?? ''
34
+ })
35
+
36
+ let latestDraftId = $derived.by(() => {
37
+ const v = versions.find((v) => v._status === 'draft' && v.latest)
38
+ return v?.id ?? ''
39
+ })
40
+
41
+ let hasDrafts = $derived(collection?.versions?.drafts != null)
42
+
43
+ async function loadVersions(page: number) {
44
+ isLoading = true
45
+ const result = await listVersions(collectionKey, recordId, { page, limit: perPage })
46
+ isLoading = false
47
+ if (result.ok) {
48
+ versions = result.data.versions ?? []
49
+ totalRecords = result.data.totalRecords ?? 0
50
+ currentPage = page
51
+ } else {
52
+ toast.error(result.error)
53
+ }
54
+ }
55
+
56
+ onMount(() => {
57
+ loadVersions(1)
58
+ })
59
+
60
+ function formatDate(iso: string): string {
61
+ if (!iso) return ''
62
+ return new Date(iso).toLocaleString()
63
+ }
64
+
65
+ function timeAgo(iso: string): string {
66
+ if (!iso) return ''
67
+ const diff = Date.now() - new Date(iso).getTime()
68
+ const seconds = Math.floor(diff / 1000)
69
+ if (seconds < 60) return `${seconds}s ago`
70
+ const minutes = Math.floor(seconds / 60)
71
+ if (minutes < 60) return `${minutes}m ago`
72
+ const hours = Math.floor(minutes / 60)
73
+ if (hours < 24) return `${hours}h ago`
74
+ const days = Math.floor(hours / 24)
75
+ return `${days}d ago`
76
+ }
77
+
78
+ function openRestore(id: string) {
79
+ restoreTargetId = id
80
+ restoreModalOpen = true
81
+ }
82
+
83
+ async function handleRestore(asDraft: boolean) {
84
+ isRestoring = true
85
+ const result = await restoreVersion(collectionKey, recordId, restoreTargetId, asDraft)
86
+ isRestoring = false
87
+ restoreModalOpen = false
88
+ if (result.ok) {
89
+ toast.success('Version restored')
90
+ await goto(resolve(`/${collectionKey}/${recordId}`))
91
+ } else {
92
+ toast.error(result.error)
93
+ }
94
+ }
95
+
96
+ function handlePageChange(p: number) {
97
+ loadVersions(p)
98
+ }
99
+ </script>
100
+
101
+ {#if !collection}
102
+ <div class="flex h-full items-center justify-center">
103
+ <p class="text-muted-foreground">Collection not found</p>
104
+ </div>
105
+ {:else if isLoading}
106
+ <div class="flex h-full items-center justify-center">
107
+ <div class="flex flex-col items-center gap-3">
108
+ <div class="h-6 w-6 rounded-full border-2 border-primary/20 border-t-primary animate-spin-slow"></div>
109
+ <p class="text-sm text-muted-foreground">Loading...</p>
110
+ </div>
111
+ </div>
112
+ {:else}
113
+ <div class="space-y-5">
114
+ <!-- Breadcrumb -->
115
+ <nav class="flex items-center gap-1.5 text-sm text-muted-foreground">
116
+ <a href={resolve(`/${collectionKey}`)} class="hover:text-foreground">{collection.labelPlural || collection.label}</a>
117
+ <span>/</span>
118
+ <a href={resolve(`/${collectionKey}/${recordId}`)} class="hover:text-foreground">{recordId}</a>
119
+ <span>/</span>
120
+ <span class="text-foreground font-medium">Versions</span>
121
+ </nav>
122
+
123
+ <h1 class="text-xl font-semibold text-foreground">Version History</h1>
124
+
125
+ {#if versions.length === 0}
126
+ <div class="flex flex-col items-center justify-center py-16">
127
+ <p class="text-sm text-muted-foreground">No versions found</p>
128
+ </div>
129
+ {:else}
130
+ <Pagination
131
+ page={currentPage}
132
+ {totalPages}
133
+ {totalRecords}
134
+ {perPage}
135
+ onPageChange={handlePageChange}
136
+ class="border-b border-border/60"
137
+ />
138
+
139
+ <div class="overflow-x-auto rounded-lg border border-border/80 bg-card shadow-sm">
140
+ <table class="w-full text-sm">
141
+ <thead>
142
+ <tr class="border-b border-border/60 bg-secondary/40">
143
+ <th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Date</th>
144
+ <th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Status</th>
145
+ <th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Age</th>
146
+ <th class="px-4 py-2.5 text-right font-medium text-muted-foreground">Actions</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ {#each versions as v (v.id)}
151
+ <tr class="border-b border-border/40 transition-colors hover:bg-secondary/30">
152
+ <td class="px-4 py-2.5">
153
+ <a
154
+ href={resolve(`/${collectionKey}/${recordId}/versions/${v.id}`)}
155
+ class="text-primary hover:underline"
156
+ >
157
+ {formatDate(v.createdAt)}
158
+ </a>
159
+ </td>
160
+ <td class="px-4 py-2.5">
161
+ <StatusPill
162
+ status={v._status ?? ''}
163
+ isLatest={v.id === latestPublishedId || v.id === latestDraftId}
164
+ />
165
+ </td>
166
+ <td class="px-4 py-2.5 text-muted-foreground">
167
+ {timeAgo(v.createdAt)}
168
+ </td>
169
+ <td class="px-4 py-2.5 text-right">
170
+ <button
171
+ type="button"
172
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
173
+ title="Restore this version"
174
+ onclick={() => openRestore(v.id)}
175
+ >
176
+ <RotateCcw class="h-4 w-4" />
177
+ </button>
178
+ </td>
179
+ </tr>
180
+ {/each}
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+
185
+ <Pagination
186
+ page={currentPage}
187
+ {totalPages}
188
+ {totalRecords}
189
+ {perPage}
190
+ onPageChange={handlePageChange}
191
+ />
192
+ {/if}
193
+ </div>
194
+
195
+ <RestoreModal
196
+ bind:open={restoreModalOpen}
197
+ {hasDrafts}
198
+ {isRestoring}
199
+ onRestore={handleRestore}
200
+ />
201
+ {/if}
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "skipLibCheck": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "allowJs": true,
14
+ "checkJs": true,
15
+ "isolatedModules": true,
16
+ "verbatimModuleSyntax": true,
17
+ "types": ["svelte", "vite/client"],
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "$lib": ["src/lib"],
21
+ "$lib/*": ["src/lib/*"]
22
+ }
23
+ },
24
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte", "vite.config.ts"]
25
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
2
+ import tailwindcss from '@tailwindcss/vite'
3
+ import { defineConfig, type Plugin } from 'vite'
4
+ import { resolve } from 'node:path'
5
+
6
+ /**
7
+ * Svelte 5 SPA — NOT SvelteKit.
8
+ *
9
+ * This config is used for @quoin-cms/admin's own standalone `pnpm dev` / `pnpm
10
+ * build` during LIBRARY development (contributors iterating on admin source
11
+ * without a real consumer). Consumer projects use @quoin-cms/cli, which wraps
12
+ * Vite with its own quoinPlugin that injects consumer-owned config +
13
+ * override components.
14
+ *
15
+ * Here we provide stub versions of the two virtual modules so `main.ts`
16
+ * imports resolve to a minimal working config.
17
+ */
18
+
19
+ function stubVirtualModulesPlugin(): Plugin {
20
+ const CONFIG_ID = 'virtual:quoin-config'
21
+ const IMPORT_MAP_ID = 'virtual:quoin-import-map'
22
+ const resolvedConfig = '\0' + CONFIG_ID
23
+ const resolvedImportMap = '\0' + IMPORT_MAP_ID
24
+
25
+ return {
26
+ name: 'quoin:stub-virtual-modules',
27
+ resolveId(id) {
28
+ if (id === CONFIG_ID) return resolvedConfig
29
+ if (id === IMPORT_MAP_ID) return resolvedImportMap
30
+ return null
31
+ },
32
+ load(id) {
33
+ if (id === resolvedConfig) {
34
+ return `export default ${JSON.stringify(
35
+ {
36
+ apiBase: '/api',
37
+ basePath: '/admin',
38
+ buildOutDir: './dist',
39
+ devPort: 5173,
40
+ brand: { name: 'Quoin', logo: null },
41
+ fields: {},
42
+ slots: {},
43
+ views: {},
44
+ pages: {},
45
+ },
46
+ null,
47
+ 2,
48
+ )};`
49
+ }
50
+ if (id === resolvedImportMap) {
51
+ return 'export default {};'
52
+ }
53
+ return null
54
+ },
55
+ }
56
+ }
57
+
58
+ export default defineConfig({
59
+ plugins: [tailwindcss(), svelte(), stubVirtualModulesPlugin()],
60
+ resolve: {
61
+ alias: {
62
+ $lib: resolve(__dirname, 'src/lib'),
63
+ },
64
+ dedupe: ['svelte', 'lexical', 'svelte-lexical', '@tanstack/svelte-query'],
65
+ },
66
+ optimizeDeps: {
67
+ include: ['lexical', 'svelte-lexical', '@tanstack/svelte-query'],
68
+ },
69
+ base: '/admin/',
70
+ build: {
71
+ outDir: 'dist',
72
+ emptyOutDir: true,
73
+ },
74
+ server: {
75
+ port: 5173,
76
+ proxy: {
77
+ '/api': 'http://localhost:8090',
78
+ },
79
+ },
80
+ })