@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.
- package/LICENSE +661 -0
- package/biome.json +62 -0
- package/dist/assets/index-C9Y5-AKj.js +33 -0
- package/dist/assets/index-uVdiUjty.css +1 -0
- package/dist/index.html +20 -0
- package/index.html +19 -0
- package/package.json +43 -0
- package/src/AdminRoot.svelte +98 -0
- package/src/app.css +211 -0
- package/src/lib/Slot.svelte +65 -0
- package/src/lib/api/auth.ts +26 -0
- package/src/lib/api/client.ts +73 -0
- package/src/lib/api/files.ts +56 -0
- package/src/lib/api/globals.ts +13 -0
- package/src/lib/api/records.ts +102 -0
- package/src/lib/api/schema.ts +7 -0
- package/src/lib/api/versions.ts +40 -0
- package/src/lib/components/AdminHeader.svelte +107 -0
- package/src/lib/components/AdminSidebar.svelte +262 -0
- package/src/lib/components/DeleteDialog.svelte +58 -0
- package/src/lib/components/DocumentEditLayout.svelte +263 -0
- package/src/lib/components/DynamicForm.svelte +74 -0
- package/src/lib/components/KpiCard.svelte +75 -0
- package/src/lib/components/MediaLibrary.svelte +311 -0
- package/src/lib/components/Pagination.svelte +78 -0
- package/src/lib/components/RangeFilter.svelte +41 -0
- package/src/lib/components/RecordGrid.svelte +123 -0
- package/src/lib/components/RecordTable.svelte +156 -0
- package/src/lib/components/cells/CheckboxCell.svelte +10 -0
- package/src/lib/components/cells/ColorCell.svelte +15 -0
- package/src/lib/components/cells/DateCell.svelte +8 -0
- package/src/lib/components/cells/RelationshipCell.svelte +20 -0
- package/src/lib/components/cells/RichTextCell.svelte +21 -0
- package/src/lib/components/cells/SelectCell.svelte +26 -0
- package/src/lib/components/cells/TextCell.svelte +8 -0
- package/src/lib/components/cells/UploadCell.svelte +34 -0
- package/src/lib/components/cells/index.ts +28 -0
- package/src/lib/components/charts/TimeSeriesChart.svelte +184 -0
- package/src/lib/components/doc/ApiView.svelte +181 -0
- package/src/lib/components/doc/Autosave.svelte +102 -0
- package/src/lib/components/doc/DocHeader.svelte +86 -0
- package/src/lib/components/doc/DocMetaStrip.svelte +103 -0
- package/src/lib/components/doc/DocTabBar.svelte +26 -0
- package/src/lib/components/doc/HeaderModeSwitch.svelte +32 -0
- package/src/lib/components/doc/PublishButton.svelte +114 -0
- package/src/lib/components/doc/ScheduleModal.svelte +110 -0
- package/src/lib/components/doc/VersionHistory.svelte +20 -0
- package/src/lib/components/fields/ArrayFieldEditor.svelte +62 -0
- package/src/lib/components/fields/BlockCard.svelte +63 -0
- package/src/lib/components/fields/BlocksFieldEditor.svelte +83 -0
- package/src/lib/components/fields/CheckboxField.svelte +27 -0
- package/src/lib/components/fields/ColorField.svelte +46 -0
- package/src/lib/components/fields/DateField.svelte +52 -0
- package/src/lib/components/fields/EmailField.svelte +30 -0
- package/src/lib/components/fields/FileField.svelte +280 -0
- package/src/lib/components/fields/JsonField.svelte +145 -0
- package/src/lib/components/fields/NumberField.svelte +44 -0
- package/src/lib/components/fields/PasswordField.svelte +38 -0
- package/src/lib/components/fields/RelationshipField.svelte +271 -0
- package/src/lib/components/fields/RichTextField.svelte +139 -0
- package/src/lib/components/fields/SelectField.svelte +33 -0
- package/src/lib/components/fields/SlugField.svelte +70 -0
- package/src/lib/components/fields/TabsField.svelte +56 -0
- package/src/lib/components/fields/TagsField.svelte +85 -0
- package/src/lib/components/fields/TextField.svelte +36 -0
- package/src/lib/components/fields/TextareaField.svelte +32 -0
- package/src/lib/components/fields/UploadField.svelte +166 -0
- package/src/lib/components/fields/UploadFieldDispatch.svelte +21 -0
- package/src/lib/components/fields/UploadGalleryField.svelte +166 -0
- package/src/lib/components/fields/index.ts +22 -0
- package/src/lib/components/fields/registry.ts +58 -0
- package/src/lib/components/lexical/CustomHTMLComponent.svelte +52 -0
- package/src/lib/components/lexical/CustomHTMLNode.ts +94 -0
- package/src/lib/components/lexical/PullQuoteComponent.svelte +73 -0
- package/src/lib/components/lexical/PullQuoteNode.ts +112 -0
- package/src/lib/components/lexical/lexical-helpers.ts +24 -0
- package/src/lib/components/lexical/nodes.ts +8 -0
- package/src/lib/components/lexical/toolbar/EditorToolbar.svelte +159 -0
- package/src/lib/components/lexical/toolbar/InsertBlockDropdown.svelte +278 -0
- package/src/lib/components/versions/CompareSelector.svelte +31 -0
- package/src/lib/components/versions/FieldDiff.svelte +141 -0
- package/src/lib/components/versions/RestoreModal.svelte +67 -0
- package/src/lib/components/versions/StatusPill.svelte +21 -0
- package/src/lib/context.svelte.ts +156 -0
- package/src/lib/router/index.svelte.ts +282 -0
- package/src/lib/router/matcher.ts +52 -0
- package/src/lib/stores/branding.svelte.ts +74 -0
- package/src/lib/stores/schema.svelte.ts +17 -0
- package/src/lib/types/schema.ts +126 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/diff.ts +112 -0
- package/src/lib/utils/dirty.svelte.ts +50 -0
- package/src/lib/utils/format.ts +28 -0
- package/src/lib/utils/json-highlight.ts +34 -0
- package/src/lib/utils/slug.ts +8 -0
- package/src/main.ts +32 -0
- package/src/views/AdminLayout.svelte +73 -0
- package/src/views/AdsAnalyticsView.svelte +152 -0
- package/src/views/CollectionEditView.svelte +117 -0
- package/src/views/CollectionListView.svelte +347 -0
- package/src/views/CollectionNewView.svelte +68 -0
- package/src/views/CustomPageView.svelte +59 -0
- package/src/views/DashboardView.svelte +370 -0
- package/src/views/GlobalEditView.svelte +100 -0
- package/src/views/LoginView.svelte +231 -0
- package/src/views/NotFoundView.svelte +9 -0
- package/src/views/VersionDetailView.svelte +307 -0
- package/src/views/VersionsListView.svelte +201 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CalendarClock } from 'lucide-svelte'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
open = $bindable(false),
|
|
6
|
+
initialValue = '',
|
|
7
|
+
isSubmitting = false,
|
|
8
|
+
onSchedule,
|
|
9
|
+
}: {
|
|
10
|
+
open?: boolean
|
|
11
|
+
initialValue?: string
|
|
12
|
+
isSubmitting?: boolean
|
|
13
|
+
onSchedule: (isoDate: string) => void
|
|
14
|
+
} = $props()
|
|
15
|
+
|
|
16
|
+
// Default: 1 hour from now, formatted for datetime-local input (YYYY-MM-DDTHH:mm)
|
|
17
|
+
function defaultValue(): string {
|
|
18
|
+
const d = new Date(Date.now() + 60 * 60 * 1000)
|
|
19
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
20
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// svelte-ignore state_referenced_locally
|
|
24
|
+
let value = $state(initialValue || defaultValue())
|
|
25
|
+
let error = $state('')
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (open && !value) value = defaultValue()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function handleSubmit() {
|
|
32
|
+
error = ''
|
|
33
|
+
if (!value) {
|
|
34
|
+
error = 'Pick a date and time'
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const picked = new Date(value)
|
|
38
|
+
if (isNaN(picked.getTime())) {
|
|
39
|
+
error = 'Invalid date'
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (picked.getTime() <= Date.now()) {
|
|
43
|
+
error = 'Time must be in the future'
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
onSchedule(picked.toISOString())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function close() {
|
|
50
|
+
open = false
|
|
51
|
+
error = ''
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#if open}
|
|
56
|
+
<div
|
|
57
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
|
58
|
+
onclick={close}
|
|
59
|
+
role="presentation"
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
class="w-full max-w-md rounded-lg border border-border bg-card shadow-xl"
|
|
63
|
+
onclick={(e) => e.stopPropagation()}
|
|
64
|
+
role="dialog"
|
|
65
|
+
aria-modal="true"
|
|
66
|
+
>
|
|
67
|
+
<div class="flex items-center gap-3 border-b border-border/60 px-5 py-4">
|
|
68
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
69
|
+
<CalendarClock class="h-5 w-5" />
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<h2 class="text-sm font-semibold text-foreground">Schedule Publish</h2>
|
|
73
|
+
<p class="text-xs text-muted-foreground">Post will publish automatically at the selected time.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="px-5 py-5 space-y-2">
|
|
78
|
+
<label class="block text-xs font-medium text-foreground" for="schedule-datetime">Publish at</label>
|
|
79
|
+
<input
|
|
80
|
+
id="schedule-datetime"
|
|
81
|
+
type="datetime-local"
|
|
82
|
+
bind:value
|
|
83
|
+
class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
84
|
+
/>
|
|
85
|
+
{#if error}
|
|
86
|
+
<p class="text-xs text-destructive">{error}</p>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="flex justify-end gap-2 border-t border-border/60 px-5 py-3">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onclick={close}
|
|
94
|
+
disabled={isSubmitting}
|
|
95
|
+
class="rounded-lg px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-secondary hover:text-foreground disabled:opacity-50"
|
|
96
|
+
>
|
|
97
|
+
Cancel
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onclick={handleSubmit}
|
|
102
|
+
disabled={isSubmitting}
|
|
103
|
+
class="rounded-lg bg-primary px-4 py-1.5 text-xs font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-60"
|
|
104
|
+
>
|
|
105
|
+
{isSubmitting ? 'Scheduling…' : 'Schedule'}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { resolve } from '$lib/router/index.svelte.js'
|
|
3
|
+
import { History } from 'lucide-svelte'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
collectionKey,
|
|
7
|
+
recordId,
|
|
8
|
+
}: {
|
|
9
|
+
collectionKey: string
|
|
10
|
+
recordId: string
|
|
11
|
+
} = $props()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<a
|
|
15
|
+
href={resolve(`/${collectionKey}/${recordId}/versions`)}
|
|
16
|
+
class="inline-flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
|
|
17
|
+
>
|
|
18
|
+
<History class="h-3.5 w-3.5" />
|
|
19
|
+
View version history
|
|
20
|
+
</a>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import BlockCard from './BlockCard.svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
field: { name: string; label?: string; labels?: { singular?: string; plural?: string } };
|
|
6
|
+
value: any[];
|
|
7
|
+
onChange: (next: any[]) => void;
|
|
8
|
+
}
|
|
9
|
+
let { field, value, onChange }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const rows = $derived(Array.isArray(value) ? value : []);
|
|
12
|
+
const singular = $derived(field.labels?.singular ?? 'Item');
|
|
13
|
+
|
|
14
|
+
function genId(): string {
|
|
15
|
+
return crypto.randomUUID(); // browser UUIDv4 — server Sanitize will upgrade to v7 if absent
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function add() {
|
|
19
|
+
onChange([...rows, { id: genId() }]);
|
|
20
|
+
}
|
|
21
|
+
function update(i: number, next: any) {
|
|
22
|
+
const out = [...rows];
|
|
23
|
+
out[i] = next;
|
|
24
|
+
onChange(out);
|
|
25
|
+
}
|
|
26
|
+
function remove(i: number) {
|
|
27
|
+
onChange(rows.filter((_, j) => j !== i));
|
|
28
|
+
}
|
|
29
|
+
function moveUp(i: number) {
|
|
30
|
+
if (i === 0) return;
|
|
31
|
+
const out = [...rows];
|
|
32
|
+
[out[i - 1], out[i]] = [out[i], out[i - 1]];
|
|
33
|
+
onChange(out);
|
|
34
|
+
}
|
|
35
|
+
function moveDown(i: number) {
|
|
36
|
+
if (i === rows.length - 1) return;
|
|
37
|
+
const out = [...rows];
|
|
38
|
+
[out[i], out[i + 1]] = [out[i + 1], out[i]];
|
|
39
|
+
onChange(out);
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class="array-field">
|
|
44
|
+
{#if field.label}<label>{field.label}</label>{/if}
|
|
45
|
+
{#each rows as row, i (row.id)}
|
|
46
|
+
<BlockCard
|
|
47
|
+
title="{singular} #{i + 1}"
|
|
48
|
+
value={row}
|
|
49
|
+
canMoveUp={i > 0}
|
|
50
|
+
canMoveDown={i < rows.length - 1}
|
|
51
|
+
onChange={(next) => update(i, next)}
|
|
52
|
+
onMoveUp={() => moveUp(i)}
|
|
53
|
+
onMoveDown={() => moveDown(i)}
|
|
54
|
+
onDelete={() => remove(i)}
|
|
55
|
+
/>
|
|
56
|
+
{/each}
|
|
57
|
+
<button type="button" onclick={add}>+ Add {singular}</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.array-field label { display: block; font-weight: 600; margin-bottom: 8px; }
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string; // e.g. "Block #1 · hero"
|
|
4
|
+
value: Record<string, any>;
|
|
5
|
+
canMoveUp: boolean;
|
|
6
|
+
canMoveDown: boolean;
|
|
7
|
+
onChange: (next: Record<string, any>) => void;
|
|
8
|
+
onMoveUp: () => void;
|
|
9
|
+
onMoveDown: () => void;
|
|
10
|
+
onDelete: () => void;
|
|
11
|
+
}
|
|
12
|
+
let { title, value, canMoveUp, canMoveDown, onChange, onMoveUp, onMoveDown, onDelete }: Props = $props();
|
|
13
|
+
|
|
14
|
+
// Strip `id` from the textarea (read-only system field).
|
|
15
|
+
const visible = $derived.by(() => {
|
|
16
|
+
const { id, ...rest } = value;
|
|
17
|
+
return rest;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let text = $state(JSON.stringify(visible, null, 2));
|
|
21
|
+
let parseError = $state('');
|
|
22
|
+
|
|
23
|
+
function save() {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
parseError = '';
|
|
27
|
+
onChange({ ...parsed, id: value.id });
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
parseError = e.message;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Re-sync textarea if value changes externally (e.g. reorder)
|
|
34
|
+
$effect(() => {
|
|
35
|
+
text = JSON.stringify(visible, null, 2);
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="card">
|
|
40
|
+
<header>
|
|
41
|
+
<span class="title">{title}</span>
|
|
42
|
+
<div class="actions">
|
|
43
|
+
<button type="button" disabled={!canMoveUp} onclick={onMoveUp} aria-label="Move up">▲</button>
|
|
44
|
+
<button type="button" disabled={!canMoveDown} onclick={onMoveDown} aria-label="Move down">▼</button>
|
|
45
|
+
<button type="button" onclick={onDelete} aria-label="Delete">×</button>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
<textarea bind:value={text} rows="6" class:error={!!parseError}></textarea>
|
|
49
|
+
{#if parseError}
|
|
50
|
+
<p class="parse-error">{parseError}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
<button type="button" onclick={save}>Save</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.card { border: 1px solid #ddd; border-radius: 4px; padding: 12px; margin-bottom: 12px; }
|
|
57
|
+
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
58
|
+
.title { font-weight: 600; font-family: monospace; font-size: 13px; }
|
|
59
|
+
.actions button { margin-left: 4px; }
|
|
60
|
+
textarea { width: 100%; font-family: monospace; font-size: 13px; }
|
|
61
|
+
textarea.error { border-color: #c00; }
|
|
62
|
+
.parse-error { color: #c00; font-size: 12px; margin: 4px 0; }
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import BlockCard from './BlockCard.svelte';
|
|
3
|
+
|
|
4
|
+
interface BlockDef {
|
|
5
|
+
slug: string;
|
|
6
|
+
label: string;
|
|
7
|
+
fields?: any[];
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
field: {
|
|
11
|
+
name: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
blocks: BlockDef[]; // resolved (registry pre-merged)
|
|
14
|
+
allowedBlocks?: string[];
|
|
15
|
+
};
|
|
16
|
+
value: any[];
|
|
17
|
+
onChange: (next: any[]) => void;
|
|
18
|
+
}
|
|
19
|
+
let { field, value, onChange }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const rows = $derived(Array.isArray(value) ? value : []);
|
|
22
|
+
let pickerSlug = $state(field.blocks[0]?.slug ?? '');
|
|
23
|
+
|
|
24
|
+
function genId(): string {
|
|
25
|
+
return crypto.randomUUID();
|
|
26
|
+
}
|
|
27
|
+
function defaultsFor(slug: string): Record<string, any> {
|
|
28
|
+
return { id: genId(), blockType: slug };
|
|
29
|
+
}
|
|
30
|
+
function add() {
|
|
31
|
+
if (!pickerSlug) return;
|
|
32
|
+
onChange([...rows, defaultsFor(pickerSlug)]);
|
|
33
|
+
}
|
|
34
|
+
function update(i: number, next: any) {
|
|
35
|
+
const out = [...rows];
|
|
36
|
+
out[i] = next;
|
|
37
|
+
onChange(out);
|
|
38
|
+
}
|
|
39
|
+
function remove(i: number) {
|
|
40
|
+
onChange(rows.filter((_, j) => j !== i));
|
|
41
|
+
}
|
|
42
|
+
function moveUp(i: number) {
|
|
43
|
+
if (i === 0) return;
|
|
44
|
+
const out = [...rows];
|
|
45
|
+
[out[i - 1], out[i]] = [out[i], out[i - 1]];
|
|
46
|
+
onChange(out);
|
|
47
|
+
}
|
|
48
|
+
function moveDown(i: number) {
|
|
49
|
+
if (i === rows.length - 1) return;
|
|
50
|
+
const out = [...rows];
|
|
51
|
+
[out[i], out[i + 1]] = [out[i + 1], out[i]];
|
|
52
|
+
onChange(out);
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="blocks-field">
|
|
57
|
+
{#if field.label}<label>{field.label}</label>{/if}
|
|
58
|
+
{#each rows as row, i (row.id)}
|
|
59
|
+
<BlockCard
|
|
60
|
+
title="Block #{i + 1} · {row.blockType ?? '?'}"
|
|
61
|
+
value={row}
|
|
62
|
+
canMoveUp={i > 0}
|
|
63
|
+
canMoveDown={i < rows.length - 1}
|
|
64
|
+
onChange={(next) => update(i, next)}
|
|
65
|
+
onMoveUp={() => moveUp(i)}
|
|
66
|
+
onMoveDown={() => moveDown(i)}
|
|
67
|
+
onDelete={() => remove(i)}
|
|
68
|
+
/>
|
|
69
|
+
{/each}
|
|
70
|
+
<div class="add-bar">
|
|
71
|
+
<select bind:value={pickerSlug}>
|
|
72
|
+
{#each field.blocks as b}
|
|
73
|
+
<option value={b.slug}>{b.label}</option>
|
|
74
|
+
{/each}
|
|
75
|
+
</select>
|
|
76
|
+
<button type="button" onclick={add}>+ Add Block</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.blocks-field label { display: block; font-weight: 600; margin-bottom: 8px; }
|
|
82
|
+
.add-bar { display: flex; gap: 8px; margin-top: 8px; }
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldSchema } from '$lib/types/schema.js'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
field,
|
|
6
|
+
value = $bindable(),
|
|
7
|
+
error,
|
|
8
|
+
}: {
|
|
9
|
+
field: FieldSchema
|
|
10
|
+
value?: boolean
|
|
11
|
+
error?: string
|
|
12
|
+
} = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div>
|
|
16
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
17
|
+
<input
|
|
18
|
+
type="checkbox"
|
|
19
|
+
bind:checked={value}
|
|
20
|
+
class="h-4 w-4 rounded border accent-primary"
|
|
21
|
+
/>
|
|
22
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
23
|
+
</label>
|
|
24
|
+
{#if error}
|
|
25
|
+
<p class="mt-1 text-xs text-destructive">{error}</p>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldSchema } from '$lib/types/schema.js'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
field,
|
|
6
|
+
value = $bindable(),
|
|
7
|
+
error,
|
|
8
|
+
}: {
|
|
9
|
+
field: FieldSchema
|
|
10
|
+
value?: string
|
|
11
|
+
error?: string
|
|
12
|
+
} = $props()
|
|
13
|
+
|
|
14
|
+
// <input type="color"> requires a valid #rrggbb value
|
|
15
|
+
let colorValue = $derived(value && /^#[0-9a-fA-F]{6}$/.test(value) ? value : '#000000')
|
|
16
|
+
|
|
17
|
+
function handleColorInput(e: Event) {
|
|
18
|
+
value = (e.target as HTMLInputElement).value
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div>
|
|
23
|
+
<label for={field.name} class="mb-1.5 block text-sm font-medium">
|
|
24
|
+
{field.label}
|
|
25
|
+
{#if field.required}<span class="text-destructive">*</span>{/if}
|
|
26
|
+
</label>
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<input
|
|
29
|
+
type="color"
|
|
30
|
+
value={colorValue}
|
|
31
|
+
oninput={handleColorInput}
|
|
32
|
+
class="h-10 w-12 cursor-pointer rounded border p-1"
|
|
33
|
+
/>
|
|
34
|
+
<input
|
|
35
|
+
id={field.name}
|
|
36
|
+
type="text"
|
|
37
|
+
bind:value
|
|
38
|
+
class="h-10 flex-1 rounded-md border bg-background px-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-ring {error ? 'border-destructive' : ''}"
|
|
39
|
+
placeholder="#000000"
|
|
40
|
+
maxlength={7}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
{#if error}
|
|
44
|
+
<p class="mt-1 text-xs text-destructive">{error}</p>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldSchema } from '$lib/types/schema.js'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
field,
|
|
6
|
+
value = $bindable(),
|
|
7
|
+
error,
|
|
8
|
+
}: {
|
|
9
|
+
field: FieldSchema
|
|
10
|
+
value?: string | null
|
|
11
|
+
error?: string
|
|
12
|
+
} = $props()
|
|
13
|
+
|
|
14
|
+
// Convert stored ISO 8601 string to the YYYY-MM-DDTHH:mm format
|
|
15
|
+
// expected by <input type="datetime-local">.
|
|
16
|
+
function toLocalInput(iso: string | null | undefined): string {
|
|
17
|
+
if (!iso) return ''
|
|
18
|
+
const d = new Date(iso)
|
|
19
|
+
if (isNaN(d.getTime())) return ''
|
|
20
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
21
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let inputValue = $derived(toLocalInput(value as any))
|
|
25
|
+
|
|
26
|
+
function onInput(e: Event) {
|
|
27
|
+
const v = (e.target as HTMLInputElement).value
|
|
28
|
+
if (!v) {
|
|
29
|
+
value = null
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const d = new Date(v)
|
|
33
|
+
value = isNaN(d.getTime()) ? null : d.toISOString()
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div>
|
|
38
|
+
<label for={field.name} class="mb-1.5 block text-sm font-medium">
|
|
39
|
+
{field.label}
|
|
40
|
+
{#if field.required}<span class="text-destructive">*</span>{/if}
|
|
41
|
+
</label>
|
|
42
|
+
<input
|
|
43
|
+
id={field.name}
|
|
44
|
+
type="datetime-local"
|
|
45
|
+
value={inputValue}
|
|
46
|
+
oninput={onInput}
|
|
47
|
+
class="h-10 w-full rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring {error ? 'border-destructive' : ''}"
|
|
48
|
+
/>
|
|
49
|
+
{#if error}
|
|
50
|
+
<p class="mt-1 text-xs text-destructive">{error}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldSchema } from '$lib/types/schema.js'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
field,
|
|
6
|
+
value = $bindable(),
|
|
7
|
+
error,
|
|
8
|
+
}: {
|
|
9
|
+
field: FieldSchema
|
|
10
|
+
value?: string
|
|
11
|
+
error?: string
|
|
12
|
+
} = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div>
|
|
16
|
+
<label for={field.name} class="mb-1.5 block text-sm font-medium">
|
|
17
|
+
{field.label}
|
|
18
|
+
{#if field.required}<span class="text-destructive">*</span>{/if}
|
|
19
|
+
</label>
|
|
20
|
+
<input
|
|
21
|
+
id={field.name}
|
|
22
|
+
type="email"
|
|
23
|
+
bind:value
|
|
24
|
+
class="h-10 w-full rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring {error ? 'border-destructive' : ''}"
|
|
25
|
+
placeholder={field.label}
|
|
26
|
+
/>
|
|
27
|
+
{#if error}
|
|
28
|
+
<p class="mt-1 text-xs text-destructive">{error}</p>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|