@hearthlog/shell 0.0.0-stage → 0.1.1
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 +235 -0
- package/package.json +51 -3
- package/src/app.template.html +184 -0
- package/src/components/ai/DecisionReviewSheet.svelte +226 -0
- package/src/components/ai/SuggestButton.svelte +23 -0
- package/src/components/ai/SuggestionChip.svelte +13 -0
- package/src/components/ai/SuggestionMark.svelte +15 -0
- package/src/components/calendar/CalendarFeedsDrawer.svelte +414 -0
- package/src/components/calendar/CalendarMonthSummary.svelte +122 -0
- package/src/components/calendar/calendar-weeks.svelte.ts +27 -0
- package/src/components/calendar/source-icons.ts +19 -0
- package/src/components/data-table/DataTableValue.svelte +14 -0
- package/src/components/data-table/EntityActionsCell.svelte +36 -0
- package/src/components/data-table/EntityRefsCell.svelte +58 -0
- package/src/components/data-table/EntityValue.svelte +42 -0
- package/src/components/data-table/entity-columns.ts +87 -0
- package/src/components/data-table/sqlite-columns.ts +42 -0
- package/src/components/entities/CalendarSourceFields.svelte +149 -0
- package/src/components/history/ChangeRecord.svelte +30 -0
- package/src/components/history/ForkComposer.svelte +174 -0
- package/src/components/history/HistoryScrubber.svelte +245 -0
- package/src/components/history/LogHistory.svelte +334 -0
- package/src/components/history/LogMap.svelte +293 -0
- package/src/components/history/LogTree.svelte +124 -0
- package/src/components/layout/AccountMenu.svelte +111 -0
- package/src/components/layout/AppearanceOptions.svelte +78 -0
- package/src/components/layout/BrandWordmark.svelte +12 -0
- package/src/components/layout/ErrorToast.svelte +34 -0
- package/src/components/layout/LogCreateDrawer.svelte +111 -0
- package/src/components/layout/LogPanel.svelte +225 -0
- package/src/components/layout/LogStatusChip.svelte +137 -0
- package/src/components/layout/MobilePageAction.svelte +44 -0
- package/src/components/layout/MovedBanner.svelte +51 -0
- package/src/components/layout/NavBar.svelte +603 -0
- package/src/components/layout/NavFlyout.svelte +132 -0
- package/src/components/layout/NavItem.svelte +36 -0
- package/src/components/layout/NavMoreDialog.svelte +179 -0
- package/src/components/layout/PageDataStatus.svelte +36 -0
- package/src/components/layout/PhoneGlance.svelte +96 -0
- package/src/components/layout/SidebarResizer.svelte +122 -0
- package/src/components/layout/SyncIndicator.svelte +24 -0
- package/src/components/layout/feature-icons.ts +24 -0
- package/src/components/layout/nav-indicator.ts +128 -0
- package/src/components/layout/sidebar-size.ts +31 -0
- package/src/components/sync/SyncIssuesDrawer.svelte +149 -0
- package/src/components/sync/SyncStateBadge.svelte +17 -0
- package/src/pages/calendar/Page.svelte +1341 -0
- package/src/pages/calendar/load.ts +77 -0
- package/src/pages/developer/DatabaseBrowser.svelte +325 -0
- package/src/pages/developer/Event.svelte +51 -0
- package/src/pages/developer/EventLogManager.svelte +268 -0
- package/src/pages/developer/IndexedDbStats.svelte +93 -0
- package/src/pages/developer/LedgerAdmin.svelte +877 -0
- package/src/pages/developer/LocalSqliteStats.svelte +56 -0
- package/src/pages/developer/Page.svelte +590 -0
- package/src/pages/developer/PendingWorkImport.svelte +49 -0
- package/src/pages/developer/PerformanceDashboard.svelte +262 -0
- package/src/pages/developer/StackInfo.svelte +117 -0
- package/src/pages/developer/SyncLog.svelte +188 -0
- package/src/pages/developer/load.ts +28 -0
- package/src/pages/entities/Page.svelte +1069 -0
- package/src/pages/entities/load.ts +18 -0
- package/src/pages/entity/Page.svelte +655 -0
- package/src/pages/entity/load.ts +79 -0
- package/src/pages/error/ErrorPage.svelte +147 -0
- package/src/pages/layout/Layout.svelte +597 -0
- package/src/pages/layout/load.ts +28 -0
- package/src/pages/redirects/history.ts +20 -0
- package/src/pages/redirects/start.ts +11 -0
- package/src/pages/redirects/today.ts +9 -0
- package/src/service-worker/index.ts +108 -0
- package/src/service-worker/offline-bundle.ts +116 -0
- package/src/service-worker/virtual.d.ts +3 -0
- package/src/styles/index.css +11 -0
- package/src/styles/shell.css +296 -0
- package/static/_headers +4 -0
- package/static/favicon.ico +0 -0
- package/static/offline.html +44 -0
- package/static/replay/sw.js +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { providePageResource } from "@hearthlog/runtime/runtime/page-data.svelte.js";
|
|
3
|
+
import PageDataStatus from "@hearthlog/shell/components/layout/PageDataStatus.svelte";
|
|
4
|
+
import DataPlaceholder from "@hearthlog/ui/components/data/DataPlaceholder.svelte";
|
|
5
|
+
import DataValue from "@hearthlog/ui/components/data/DataValue.svelte";
|
|
6
|
+
import {
|
|
7
|
+
executeLocalCommand,
|
|
8
|
+
type LocalCommandResult,
|
|
9
|
+
} from "@hearthlog/runtime/commands/index.js";
|
|
10
|
+
|
|
11
|
+
import { refreshPageData as invalidate } from "@hearthlog/runtime/runtime/refresh.js";
|
|
12
|
+
import type { EntityTypeRecord } from "@hearthlog/runtime/db/dynamic-ddl.js";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
interface EntityTypeWithStatic extends EntityTypeRecord {
|
|
16
|
+
is_static?: boolean;
|
|
17
|
+
}
|
|
18
|
+
import { generateExportSnippet } from "@hearthlog/runtime/event/code-export.js";
|
|
19
|
+
import type { DynamicFieldDefinition } from "@hearthlog/runtime/event/meta-entity-types.js";
|
|
20
|
+
import { fade, fly } from "svelte/transition";
|
|
21
|
+
import { onMount } from "svelte";
|
|
22
|
+
import CommandConfirmDialog from "@hearthlog/ui/components/commands/CommandConfirmDialog.svelte";
|
|
23
|
+
import CommandDrawer from "@hearthlog/ui/components/commands/CommandDrawer.svelte";
|
|
24
|
+
import CalendarSourceFields from "@hearthlog/shell/components/entities/CalendarSourceFields.svelte";
|
|
25
|
+
import { parseCalendarConfig } from "@hearthlog/contracts/calendar/config";
|
|
26
|
+
import {
|
|
27
|
+
calendarDraftError,
|
|
28
|
+
calendarFromDraft,
|
|
29
|
+
draftFromCalendar,
|
|
30
|
+
emptyCalendarDraft,
|
|
31
|
+
type CalendarSourceDraft,
|
|
32
|
+
} from "@hearthlog/runtime/calendar/draft.js";
|
|
33
|
+
import CommandOnly from "@hearthlog/ui/components/commands/CommandOnly.svelte";
|
|
34
|
+
import Modal from "@hearthlog/ui/components/Modal.svelte";
|
|
35
|
+
import EmptyState from "@hearthlog/ui/components/EmptyState.svelte";
|
|
36
|
+
import PageHeader from "@hearthlog/ui/components/PageHeader.svelte";
|
|
37
|
+
import Button from "@hearthlog/ui/components/ui/Button.svelte";
|
|
38
|
+
import Input from "@hearthlog/ui/components/ui/Input.svelte";
|
|
39
|
+
import Checkbox from "@hearthlog/ui/components/ui/Checkbox.svelte";
|
|
40
|
+
import { notify } from "@hearthlog/ui/state/notifications.svelte.js";
|
|
41
|
+
import { contextualHref } from "@hearthlog/runtime/runtime/view.js";
|
|
42
|
+
import { tenant } from "@hearthlog/contracts/tenant/config";
|
|
43
|
+
import IconSearch from "~icons/mdi/magnify";
|
|
44
|
+
import IconPlus from "~icons/mdi/plus";
|
|
45
|
+
import IconPencil from "~icons/mdi/pencil-outline";
|
|
46
|
+
import IconCode from "~icons/mdi/code-tags";
|
|
47
|
+
import IconTrash from "~icons/mdi/trash-can-outline";
|
|
48
|
+
import IconArrowRight from "~icons/mdi/arrow-right";
|
|
49
|
+
|
|
50
|
+
let { data: routeData } = $props();
|
|
51
|
+
const resource = $derived(routeData.pageResource);
|
|
52
|
+
const data = $derived(resource.data);
|
|
53
|
+
providePageResource(() => resource);
|
|
54
|
+
let entityTypes = $derived(data.entityTypes as EntityTypeWithStatic[]);
|
|
55
|
+
const entityHref = (name: string) => contextualHref(`/entities/${name}`);
|
|
56
|
+
|
|
57
|
+
function safeParse(json: string, name: string): any[] {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(json);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`[Entities] Failed to parse fields for ${name}:`, e);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onMount(() => {
|
|
67
|
+
// console.log("[Entities] onMount");
|
|
68
|
+
// return () => console.log("[Entities] onDestroy");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Filter and Search
|
|
72
|
+
let searchQuery = $state("");
|
|
73
|
+
let filteredEntityTypes = $derived(
|
|
74
|
+
entityTypes.filter(
|
|
75
|
+
(et: EntityTypeRecord) =>
|
|
76
|
+
et.display_name
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.includes(searchQuery.toLowerCase()) ||
|
|
79
|
+
et.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Form state
|
|
84
|
+
let isCreating = $state(false);
|
|
85
|
+
let newName = $state("");
|
|
86
|
+
let newDisplayName = $state("");
|
|
87
|
+
let newFields = $state<DynamicFieldDefinition[]>([]);
|
|
88
|
+
let calendarDraft = $state<CalendarSourceDraft>(emptyCalendarDraft());
|
|
89
|
+
const calendarDraftInvalid = $derived(
|
|
90
|
+
calendarDraft.enabled &&
|
|
91
|
+
calendarDraftError(calendarDraft, newFields) !== null,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Export modal state
|
|
95
|
+
let showExportModal = $state(false);
|
|
96
|
+
let exportCode = $state("");
|
|
97
|
+
|
|
98
|
+
// Confirmation dialog states
|
|
99
|
+
let showArchiveConfirm = $state(false);
|
|
100
|
+
let itemToArchive = $state<EntityTypeRecord | null>(null);
|
|
101
|
+
let showRedefineConfirm = $state(false);
|
|
102
|
+
|
|
103
|
+
// Field being edited
|
|
104
|
+
let editingFieldIndex = $state<number | null>(null);
|
|
105
|
+
let editingField = $state<DynamicFieldDefinition>({
|
|
106
|
+
name: "",
|
|
107
|
+
type: "string",
|
|
108
|
+
payloadKey: "",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function resetForm() {
|
|
112
|
+
newName = "";
|
|
113
|
+
newDisplayName = "";
|
|
114
|
+
newFields = [];
|
|
115
|
+
calendarDraft = emptyCalendarDraft();
|
|
116
|
+
isCreating = false;
|
|
117
|
+
editingFieldIndex = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addField() {
|
|
121
|
+
if (!editingField.name) return;
|
|
122
|
+
|
|
123
|
+
const payloadKey =
|
|
124
|
+
editingField.payloadKey || toCamelCase(editingField.name);
|
|
125
|
+
|
|
126
|
+
const field: DynamicFieldDefinition = {
|
|
127
|
+
...editingField,
|
|
128
|
+
name: toSnakeCase(editingField.name),
|
|
129
|
+
payloadKey,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (editingFieldIndex !== null) {
|
|
133
|
+
newFields[editingFieldIndex] = field;
|
|
134
|
+
} else {
|
|
135
|
+
newFields = [...newFields, field];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
editingField = { name: "", type: "string", payloadKey: "" };
|
|
139
|
+
editingFieldIndex = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeField(index: number) {
|
|
143
|
+
newFields = newFields.filter((_, i) => i !== index);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function editField(index: number) {
|
|
147
|
+
editingFieldIndex = index;
|
|
148
|
+
editingField = { ...newFields[index] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function toSnakeCase(str: string): string {
|
|
152
|
+
return str
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/\s+/g, "_")
|
|
155
|
+
.replace(/[^a-z0-9_]/g, "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toCamelCase(str: string): string {
|
|
159
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function reportProjectionState(result: LocalCommandResult): void {
|
|
163
|
+
if (result.projectionPending) {
|
|
164
|
+
notify.info(
|
|
165
|
+
"Definition saved locally. Its database projection will retry on startup.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function handleCreate() {
|
|
171
|
+
if (!newName.trim() || newFields.length === 0) return;
|
|
172
|
+
if (calendarDraftInvalid) return;
|
|
173
|
+
|
|
174
|
+
const name = toSnakeCase(newName);
|
|
175
|
+
const tableName = name + "s";
|
|
176
|
+
const calendar = calendarFromDraft(calendarDraft);
|
|
177
|
+
|
|
178
|
+
const result = await executeLocalCommand({
|
|
179
|
+
id: crypto.randomUUID(),
|
|
180
|
+
type: "ENTITY_TYPE_DEFINED",
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
payload: {
|
|
183
|
+
id: crypto.randomUUID(),
|
|
184
|
+
name,
|
|
185
|
+
displayName: newDisplayName || newName,
|
|
186
|
+
tableName,
|
|
187
|
+
fields: newFields,
|
|
188
|
+
...(calendar.length > 0 ? { calendar } : {}),
|
|
189
|
+
},
|
|
190
|
+
} as any);
|
|
191
|
+
reportProjectionState(result);
|
|
192
|
+
|
|
193
|
+
await invalidate("app:entity-types");
|
|
194
|
+
resetForm();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handleArchive(entityType: EntityTypeRecord) {
|
|
198
|
+
itemToArchive = entityType;
|
|
199
|
+
showArchiveConfirm = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function executeArchive() {
|
|
203
|
+
if (!itemToArchive) return;
|
|
204
|
+
const entityType = itemToArchive;
|
|
205
|
+
showArchiveConfirm = false;
|
|
206
|
+
itemToArchive = null;
|
|
207
|
+
|
|
208
|
+
const result = await executeLocalCommand({
|
|
209
|
+
id: crypto.randomUUID(),
|
|
210
|
+
type: "ENTITY_TYPE_ARCHIVED",
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
payload: {
|
|
213
|
+
id: entityType.id,
|
|
214
|
+
},
|
|
215
|
+
} as any);
|
|
216
|
+
reportProjectionState(result);
|
|
217
|
+
|
|
218
|
+
await invalidate("app:entity-types");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleExport(entityType: EntityTypeRecord) {
|
|
222
|
+
exportCode = generateExportSnippet(entityType);
|
|
223
|
+
showExportModal = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function copyToClipboard() {
|
|
227
|
+
await navigator.clipboard.writeText(exportCode);
|
|
228
|
+
notify.success("Entity definition copied");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Edit Mode - Destructive Redesign
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
let isEditing = $state<EntityTypeRecord | null>(null);
|
|
236
|
+
|
|
237
|
+
function startEdit(entityType: EntityTypeRecord) {
|
|
238
|
+
isEditing = entityType;
|
|
239
|
+
newName = entityType.name;
|
|
240
|
+
newDisplayName = entityType.display_name;
|
|
241
|
+
newFields = safeParse(entityType.fields_json, entityType.name);
|
|
242
|
+
calendarDraft = draftFromCalendar(
|
|
243
|
+
parseCalendarConfig(entityType.calendar_json),
|
|
244
|
+
);
|
|
245
|
+
editingFieldIndex = null;
|
|
246
|
+
editingField = { name: "", type: "string", payloadKey: "" };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cancelEdit() {
|
|
250
|
+
isEditing = null;
|
|
251
|
+
resetForm();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function handleRedefinition() {
|
|
255
|
+
if (!isEditing) return;
|
|
256
|
+
showRedefineConfirm = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function executeRedefinition() {
|
|
260
|
+
if (!isEditing) return;
|
|
261
|
+
showRedefineConfirm = false;
|
|
262
|
+
if (calendarDraftInvalid) return;
|
|
263
|
+
|
|
264
|
+
// Archive and replace the definition as one durable local command.
|
|
265
|
+
const name = toSnakeCase(newName);
|
|
266
|
+
const tableName = name + "s";
|
|
267
|
+
const timestamp = Date.now();
|
|
268
|
+
const calendar = calendarFromDraft(calendarDraft);
|
|
269
|
+
const result = await executeLocalCommand([
|
|
270
|
+
{
|
|
271
|
+
id: crypto.randomUUID(),
|
|
272
|
+
type: "ENTITY_TYPE_ARCHIVED",
|
|
273
|
+
timestamp,
|
|
274
|
+
payload: { id: isEditing.id },
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: crypto.randomUUID(),
|
|
278
|
+
type: "ENTITY_TYPE_DEFINED",
|
|
279
|
+
timestamp: timestamp + 1,
|
|
280
|
+
payload: {
|
|
281
|
+
id: crypto.randomUUID(),
|
|
282
|
+
name,
|
|
283
|
+
displayName: newDisplayName || newName,
|
|
284
|
+
tableName,
|
|
285
|
+
fields: newFields,
|
|
286
|
+
...(calendar.length > 0 ? { calendar } : {}),
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
] as any);
|
|
290
|
+
reportProjectionState(result);
|
|
291
|
+
|
|
292
|
+
await invalidate("app:entity-types");
|
|
293
|
+
|
|
294
|
+
isEditing = null;
|
|
295
|
+
resetForm();
|
|
296
|
+
}
|
|
297
|
+
</script>
|
|
298
|
+
|
|
299
|
+
<svelte:head>
|
|
300
|
+
<title>Entity Types | stor</title>
|
|
301
|
+
</svelte:head>
|
|
302
|
+
|
|
303
|
+
<PageDataStatus {resource} />
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
<div class="flex h-full flex-col overflow-hidden bg-[var(--stor-bg)]">
|
|
307
|
+
<!-- Header Area -->
|
|
308
|
+
{#snippet entityDescription()}
|
|
309
|
+
<DataValue>{filteredEntityTypes.length}</DataValue>
|
|
310
|
+
<DataValue>{filteredEntityTypes.length === 1 ? ' collection' : ' collections'}</DataValue>
|
|
311
|
+
{#if searchQuery.trim()}<span aria-hidden="true"> · </span>matching “{searchQuery.trim()}”{/if}
|
|
312
|
+
{/snippet}
|
|
313
|
+
<PageHeader title="Entities" class="shrink-0 px-3 pt-4 pb-2" context={entityDescription}>
|
|
314
|
+
<label class="relative min-w-0">
|
|
315
|
+
<span class="sr-only">Search types</span>
|
|
316
|
+
<IconSearch class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--stor-muted-text)]" />
|
|
317
|
+
<Input placeholder="Search types…" bind:value={searchQuery} class="pl-9 md:w-64" />
|
|
318
|
+
</label>
|
|
319
|
+
<CommandOnly>
|
|
320
|
+
{#if tenant.dynamicEntities}
|
|
321
|
+
<Button onclick={() => (isCreating = true)}><IconPlus class="h-4 w-4" /> New type</Button>
|
|
322
|
+
{/if}
|
|
323
|
+
</CommandOnly>
|
|
324
|
+
</PageHeader>
|
|
325
|
+
|
|
326
|
+
<!-- Main Content Grid -->
|
|
327
|
+
<div class="flex-1 overflow-y-auto p-3">
|
|
328
|
+
<div class="w-full">
|
|
329
|
+
{#if resource.loading}
|
|
330
|
+
<DataPlaceholder rows={5} />
|
|
331
|
+
{:else if filteredEntityTypes.length === 0}
|
|
332
|
+
<div in:fade>
|
|
333
|
+
<EmptyState
|
|
334
|
+
title="No entity types found"
|
|
335
|
+
description="Create a custom data structure to start storing specialized records."
|
|
336
|
+
>
|
|
337
|
+
<CommandOnly>
|
|
338
|
+
{#if tenant.dynamicEntities}<Button onclick={() => (isCreating = true)}>Create entity type</Button>{/if}
|
|
339
|
+
</CommandOnly>
|
|
340
|
+
</EmptyState>
|
|
341
|
+
</div>
|
|
342
|
+
{:else}
|
|
343
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
344
|
+
{#each filteredEntityTypes as entityType (entityType.id)}
|
|
345
|
+
{@const fields = safeParse(
|
|
346
|
+
entityType.fields_json,
|
|
347
|
+
entityType.name,
|
|
348
|
+
)}
|
|
349
|
+
<article class="stor-panel group flex flex-col p-[var(--stor-panel-padding)] transition-shadow hover:shadow-[var(--stor-shadow-md)]">
|
|
350
|
+
<div class="flex items-start justify-between gap-3">
|
|
351
|
+
<div class="min-w-0">
|
|
352
|
+
<a
|
|
353
|
+
href={entityHref(entityType.name)}
|
|
354
|
+
class="block truncate text-base font-semibold text-[var(--stor-ink)] transition-colors hover:text-[var(--stor-accent-strong)]"
|
|
355
|
+
>
|
|
356
|
+
{entityType.display_name}
|
|
357
|
+
</a>
|
|
358
|
+
<p class="mt-0.5 truncate font-mono text-[11px] uppercase tracking-wider text-[var(--stor-muted-text)]">
|
|
359
|
+
{entityType.name}
|
|
360
|
+
</p>
|
|
361
|
+
</div>
|
|
362
|
+
<span class="shrink-0 rounded-full bg-[var(--stor-surface-muted)] px-2.5 py-1 text-[10.5px] font-semibold uppercase tracking-wide text-[var(--stor-muted-text)]">
|
|
363
|
+
{fields.length} {fields.length === 1 ? "field" : "fields"}
|
|
364
|
+
</span>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<ul class="mt-3 flex flex-wrap gap-1.5">
|
|
368
|
+
{#each fields.slice(0, 3) as field}
|
|
369
|
+
<li class="inline-flex items-center gap-1.5 rounded-lg py-1 pr-2 text-xs">
|
|
370
|
+
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--stor-accent)]"></span>
|
|
371
|
+
<span class="font-medium text-[var(--stor-ink)]">{field.name}</span>
|
|
372
|
+
<span class="font-mono text-[10.5px] text-[var(--stor-muted-text)]">{field.type}</span>
|
|
373
|
+
</li>
|
|
374
|
+
{/each}
|
|
375
|
+
{#if fields.length > 3}
|
|
376
|
+
<li class="inline-flex items-center rounded-lg px-2 py-1 text-xs font-medium text-[var(--stor-muted-text)]">
|
|
377
|
+
+{fields.length - 3} more
|
|
378
|
+
</li>
|
|
379
|
+
{/if}
|
|
380
|
+
</ul>
|
|
381
|
+
|
|
382
|
+
<div class="mt-4 flex items-center justify-between gap-2 border-t border-[var(--stor-border)] pt-3">
|
|
383
|
+
<a
|
|
384
|
+
href={entityHref(entityType.name)}
|
|
385
|
+
class="group/link inline-flex items-center gap-1 text-sm font-semibold text-[var(--stor-accent-strong)] transition hover:text-[var(--stor-accent)]"
|
|
386
|
+
>
|
|
387
|
+
Open records
|
|
388
|
+
<IconArrowRight class="h-4 w-4 transition-transform group-hover/link:translate-x-0.5" />
|
|
389
|
+
</a>
|
|
390
|
+
{#if !entityType.is_static}
|
|
391
|
+
<div class="flex items-center gap-0.5">
|
|
392
|
+
<CommandOnly>
|
|
393
|
+
<Button
|
|
394
|
+
variant="ghost"
|
|
395
|
+
size="icon"
|
|
396
|
+
onclick={() => startEdit(entityType)}
|
|
397
|
+
title="Edit schema"
|
|
398
|
+
aria-label={`Edit ${entityType.display_name} schema`}
|
|
399
|
+
>
|
|
400
|
+
<IconPencil class="h-4 w-4" />
|
|
401
|
+
</Button>
|
|
402
|
+
</CommandOnly>
|
|
403
|
+
<Button
|
|
404
|
+
variant="ghost"
|
|
405
|
+
size="icon"
|
|
406
|
+
onclick={() => handleExport(entityType)}
|
|
407
|
+
title="Export code"
|
|
408
|
+
aria-label={`Export ${entityType.display_name} code`}
|
|
409
|
+
>
|
|
410
|
+
<IconCode class="h-4 w-4" />
|
|
411
|
+
</Button>
|
|
412
|
+
<CommandOnly>
|
|
413
|
+
<Button
|
|
414
|
+
variant="ghost"
|
|
415
|
+
size="icon"
|
|
416
|
+
onclick={() => handleArchive(entityType)}
|
|
417
|
+
class="hover:bg-[var(--stor-negative-soft)] hover:text-[var(--stor-negative)]"
|
|
418
|
+
title="Archive"
|
|
419
|
+
aria-label={`Archive ${entityType.display_name}`}
|
|
420
|
+
>
|
|
421
|
+
<IconTrash class="h-4 w-4" />
|
|
422
|
+
</Button>
|
|
423
|
+
</CommandOnly>
|
|
424
|
+
</div>
|
|
425
|
+
{:else}
|
|
426
|
+
<span class="select-none rounded-full bg-[var(--stor-surface-muted)] px-2.5 py-1 text-[10.5px] font-semibold uppercase tracking-wide text-[var(--stor-muted-text)]">
|
|
427
|
+
Built-in
|
|
428
|
+
</span>
|
|
429
|
+
{/if}
|
|
430
|
+
</div>
|
|
431
|
+
</article>
|
|
432
|
+
{/each}
|
|
433
|
+
</div>
|
|
434
|
+
{/if}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<CommandDrawer
|
|
440
|
+
show={isCreating}
|
|
441
|
+
size="lg"
|
|
442
|
+
title="Define new entity"
|
|
443
|
+
description="Create a reusable schema for a new kind of record."
|
|
444
|
+
onClose={resetForm}
|
|
445
|
+
>
|
|
446
|
+
<div class="space-y-8">
|
|
447
|
+
<!-- Basic Info -->
|
|
448
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
449
|
+
<div class="space-y-1.5">
|
|
450
|
+
<label class="text-sm font-semibold text-[var(--stor-ink)] ml-1"
|
|
451
|
+
>Entity ID (e.g. task)</label
|
|
452
|
+
>
|
|
453
|
+
<input
|
|
454
|
+
type="text"
|
|
455
|
+
placeholder="snake_case_name"
|
|
456
|
+
bind:value={newName}
|
|
457
|
+
class="stor-native-field"
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="space-y-1.5">
|
|
461
|
+
<label class="text-sm font-semibold text-[var(--stor-ink)] ml-1"
|
|
462
|
+
>Display Name (e.g. Task)</label
|
|
463
|
+
>
|
|
464
|
+
<input
|
|
465
|
+
type="text"
|
|
466
|
+
placeholder="Pretty Name"
|
|
467
|
+
bind:value={newDisplayName}
|
|
468
|
+
class="stor-native-field"
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<!-- Fields Management -->
|
|
474
|
+
<div class="space-y-4">
|
|
475
|
+
<div class="flex items-center justify-between">
|
|
476
|
+
<h3 class="text-base font-semibold text-[var(--stor-ink)]">
|
|
477
|
+
Fields Configuration
|
|
478
|
+
</h3>
|
|
479
|
+
<span
|
|
480
|
+
class="text-xs bg-[var(--stor-surface-muted)] text-[var(--stor-muted-text)] px-3 py-1 rounded-full font-semibold uppercase tracking-wide"
|
|
481
|
+
>
|
|
482
|
+
{newFields.length} total fields
|
|
483
|
+
</span>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div
|
|
487
|
+
class="space-y-3 rounded-xl bg-[var(--stor-surface-muted)] p-4"
|
|
488
|
+
>
|
|
489
|
+
<!-- Field Form -->
|
|
490
|
+
<div class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
|
|
491
|
+
<div class="md:col-span-3 space-y-1">
|
|
492
|
+
<label
|
|
493
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
494
|
+
>Field Name</label
|
|
495
|
+
>
|
|
496
|
+
<input
|
|
497
|
+
type="text"
|
|
498
|
+
placeholder="title"
|
|
499
|
+
bind:value={editingField.name}
|
|
500
|
+
class="stor-native-field"
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="md:col-span-2 space-y-1">
|
|
504
|
+
<label
|
|
505
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
506
|
+
>Type</label
|
|
507
|
+
>
|
|
508
|
+
<select
|
|
509
|
+
bind:value={editingField.type}
|
|
510
|
+
class="stor-native-field"
|
|
511
|
+
>
|
|
512
|
+
<option value="string">String</option>
|
|
513
|
+
<option value="number">Number</option>
|
|
514
|
+
<option value="boolean">Boolean</option>
|
|
515
|
+
<option value="datetime">Datetime</option>
|
|
516
|
+
<option value="reference">Reference</option>
|
|
517
|
+
</select>
|
|
518
|
+
</div>
|
|
519
|
+
{#if editingField.type === "reference"}
|
|
520
|
+
<div class="md:col-span-3 space-y-1">
|
|
521
|
+
<label
|
|
522
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
523
|
+
>References</label
|
|
524
|
+
>
|
|
525
|
+
<select
|
|
526
|
+
bind:value={editingField.referenceTo}
|
|
527
|
+
class="stor-native-field"
|
|
528
|
+
>
|
|
529
|
+
<option value="">Select entity...</option>
|
|
530
|
+
{#each entityTypes as et}
|
|
531
|
+
<option value={et.name}
|
|
532
|
+
>{et.display_name}</option
|
|
533
|
+
>
|
|
534
|
+
{/each}
|
|
535
|
+
</select>
|
|
536
|
+
</div>
|
|
537
|
+
<div class="md:col-span-2 space-y-1">
|
|
538
|
+
<label
|
|
539
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
540
|
+
>Default</label
|
|
541
|
+
>
|
|
542
|
+
<input
|
|
543
|
+
type="text"
|
|
544
|
+
placeholder="optional"
|
|
545
|
+
bind:value={editingField.default}
|
|
546
|
+
class="stor-native-field"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
{:else if editingField.type === "datetime"}
|
|
550
|
+
<div class="md:col-span-3 space-y-1">
|
|
551
|
+
<label
|
|
552
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
553
|
+
>Default</label
|
|
554
|
+
>
|
|
555
|
+
<div
|
|
556
|
+
class="flex items-center gap-2 px-3 py-2 bg-[var(--stor-surface)] border border-[var(--stor-border)] rounded-lg cursor-pointer hover:bg-[var(--stor-surface-muted)] transition-colors whitespace-nowrap"
|
|
557
|
+
>
|
|
558
|
+
<Checkbox
|
|
559
|
+
checked={editingField.default ===
|
|
560
|
+
"datetime('now')"}
|
|
561
|
+
onCheckedChange={(checked) =>
|
|
562
|
+
(editingField.default = checked
|
|
563
|
+
? "datetime('now')"
|
|
564
|
+
: undefined)}
|
|
565
|
+
class="h-4 w-4 rounded"
|
|
566
|
+
/>
|
|
567
|
+
<span class="text-sm text-[var(--stor-muted-text)]">now()</span
|
|
568
|
+
>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
{:else}
|
|
572
|
+
<div class="md:col-span-3 space-y-1">
|
|
573
|
+
<label
|
|
574
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
575
|
+
>Default</label
|
|
576
|
+
>
|
|
577
|
+
<input
|
|
578
|
+
type="text"
|
|
579
|
+
placeholder="optional"
|
|
580
|
+
bind:value={editingField.default}
|
|
581
|
+
class="stor-native-field"
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
{/if}
|
|
585
|
+
<div class="md:col-span-2">
|
|
586
|
+
<Button onclick={addField} class="w-full">
|
|
587
|
+
{editingFieldIndex !== null ? "Update" : "Add"}
|
|
588
|
+
</Button>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- Fields List -->
|
|
593
|
+
{#if newFields.length > 0}
|
|
594
|
+
<div class="mt-6 space-y-2">
|
|
595
|
+
{#each newFields as field, index}
|
|
596
|
+
<div
|
|
597
|
+
class="flex items-center gap-3 p-3 bg-[var(--stor-surface)] border border-[var(--stor-border)] rounded-lg group/item"
|
|
598
|
+
in:fly={{ x: -10, duration: 200 }}
|
|
599
|
+
>
|
|
600
|
+
<div
|
|
601
|
+
class="w-8 h-8 bg-[var(--stor-surface-muted)] rounded-lg flex items-center justify-center text-xs font-bold text-[var(--stor-muted-text)]"
|
|
602
|
+
>
|
|
603
|
+
{index + 1}
|
|
604
|
+
</div>
|
|
605
|
+
<div class="flex-1">
|
|
606
|
+
<div class="flex items-center gap-2">
|
|
607
|
+
<span
|
|
608
|
+
class="font-semibold text-sm text-[var(--stor-ink)]"
|
|
609
|
+
>{field.name}</span
|
|
610
|
+
>
|
|
611
|
+
<span
|
|
612
|
+
class="text-[10.5px] bg-[var(--stor-surface-muted)] text-[var(--stor-muted-text)] px-1.5 py-0.5 rounded font-mono uppercase"
|
|
613
|
+
>{field.type}</span
|
|
614
|
+
>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
<div
|
|
618
|
+
class="flex items-center gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover/item:opacity-100 sm:group-focus-within/item:opacity-100"
|
|
619
|
+
>
|
|
620
|
+
<button
|
|
621
|
+
onclick={() => editField(index)}
|
|
622
|
+
class="p-1.5 text-[var(--stor-muted-text)] hover:text-[var(--stor-ink)] hover:bg-[var(--stor-surface-muted)] rounded-lg transition-all"
|
|
623
|
+
>
|
|
624
|
+
<svg
|
|
625
|
+
class="w-4 h-4"
|
|
626
|
+
fill="none"
|
|
627
|
+
stroke="currentColor"
|
|
628
|
+
viewBox="0 0 24 24"
|
|
629
|
+
>
|
|
630
|
+
<path
|
|
631
|
+
stroke-linecap="round"
|
|
632
|
+
stroke-linejoin="round"
|
|
633
|
+
stroke-width="2"
|
|
634
|
+
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
635
|
+
/>
|
|
636
|
+
</svg>
|
|
637
|
+
</button>
|
|
638
|
+
<button
|
|
639
|
+
onclick={() => removeField(index)}
|
|
640
|
+
class="p-1.5 text-[var(--stor-muted-text)] hover:text-[var(--stor-negative)] hover:bg-[var(--stor-negative-soft)] rounded-lg transition-all"
|
|
641
|
+
>
|
|
642
|
+
<svg
|
|
643
|
+
class="w-4 h-4"
|
|
644
|
+
fill="none"
|
|
645
|
+
stroke="currentColor"
|
|
646
|
+
viewBox="0 0 24 24"
|
|
647
|
+
>
|
|
648
|
+
<path
|
|
649
|
+
stroke-linecap="round"
|
|
650
|
+
stroke-linejoin="round"
|
|
651
|
+
stroke-width="2"
|
|
652
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
653
|
+
/>
|
|
654
|
+
</svg>
|
|
655
|
+
</button>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
{/each}
|
|
659
|
+
</div>
|
|
660
|
+
{/if}
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<CalendarSourceFields
|
|
665
|
+
fields={newFields}
|
|
666
|
+
bind:draft={calendarDraft}
|
|
667
|
+
defaultLabel={newDisplayName}
|
|
668
|
+
/>
|
|
669
|
+
|
|
670
|
+
</div>
|
|
671
|
+
{#snippet footer()}
|
|
672
|
+
<Button variant="secondary" onclick={resetForm}>Discard</Button>
|
|
673
|
+
<Button
|
|
674
|
+
onclick={handleCreate}
|
|
675
|
+
disabled={!newName.trim() || newFields.length === 0 || calendarDraftInvalid}
|
|
676
|
+
>
|
|
677
|
+
Define Type
|
|
678
|
+
</Button>
|
|
679
|
+
|
|
680
|
+
{/snippet}
|
|
681
|
+
</CommandDrawer>
|
|
682
|
+
|
|
683
|
+
<CommandDrawer
|
|
684
|
+
show={!!isEditing}
|
|
685
|
+
size="lg"
|
|
686
|
+
title="Edit entity schema"
|
|
687
|
+
description="Review the schema carefully before redefining this entity type."
|
|
688
|
+
onClose={cancelEdit}
|
|
689
|
+
>
|
|
690
|
+
{#if isEditing}
|
|
691
|
+
<div class="space-y-8">
|
|
692
|
+
<!-- Warning Banner -->
|
|
693
|
+
<div
|
|
694
|
+
class="px-8 py-4 bg-[var(--stor-warning-soft)] border-b border-[var(--stor-border)] -mx-6 -mt-6 mb-6"
|
|
695
|
+
>
|
|
696
|
+
<div class="flex gap-3 items-start">
|
|
697
|
+
<svg
|
|
698
|
+
class="w-5 h-5 text-[var(--stor-warning)] flex-shrink-0 mt-0.5"
|
|
699
|
+
fill="none"
|
|
700
|
+
stroke="currentColor"
|
|
701
|
+
viewBox="0 0 24 24"
|
|
702
|
+
>
|
|
703
|
+
<path
|
|
704
|
+
stroke-linecap="round"
|
|
705
|
+
stroke-linejoin="round"
|
|
706
|
+
stroke-width="2"
|
|
707
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
708
|
+
/>
|
|
709
|
+
</svg>
|
|
710
|
+
<div>
|
|
711
|
+
<h3 class="font-bold text-[var(--stor-warning)] text-sm">
|
|
712
|
+
Destructive Operation
|
|
713
|
+
</h3>
|
|
714
|
+
<p class="text-[var(--stor-warning)] text-xs mt-0.5">
|
|
715
|
+
Saving changes will <strong
|
|
716
|
+
>delete all existing {isEditing.display_name} data</strong
|
|
717
|
+
>. This is intended for the design phase only.
|
|
718
|
+
</p>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<!-- Basic Info -->
|
|
724
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
725
|
+
<div class="space-y-1.5">
|
|
726
|
+
<label class="text-sm font-semibold text-[var(--stor-ink)] ml-1"
|
|
727
|
+
>Entity ID</label
|
|
728
|
+
>
|
|
729
|
+
<input
|
|
730
|
+
type="text"
|
|
731
|
+
placeholder="snake_case_name"
|
|
732
|
+
bind:value={newName}
|
|
733
|
+
disabled
|
|
734
|
+
class="stor-native-field"
|
|
735
|
+
/>
|
|
736
|
+
<p class="text-[10.5px] text-[var(--stor-muted-text)] ml-1">
|
|
737
|
+
Cannot be changed
|
|
738
|
+
</p>
|
|
739
|
+
</div>
|
|
740
|
+
<div class="space-y-1.5">
|
|
741
|
+
<label class="text-sm font-semibold text-[var(--stor-ink)] ml-1"
|
|
742
|
+
>Display Name</label
|
|
743
|
+
>
|
|
744
|
+
<input
|
|
745
|
+
type="text"
|
|
746
|
+
placeholder="Pretty Name"
|
|
747
|
+
bind:value={newDisplayName}
|
|
748
|
+
class="stor-native-field"
|
|
749
|
+
/>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<!-- Fields Management -->
|
|
754
|
+
<div class="space-y-4">
|
|
755
|
+
<div class="flex items-center justify-between">
|
|
756
|
+
<h3 class="text-base font-semibold text-[var(--stor-ink)]">
|
|
757
|
+
Fields Configuration
|
|
758
|
+
</h3>
|
|
759
|
+
<span
|
|
760
|
+
class="text-xs bg-[var(--stor-surface-muted)] text-[var(--stor-muted-text)] px-3 py-1 rounded-full font-semibold uppercase tracking-wide"
|
|
761
|
+
>
|
|
762
|
+
{newFields.length} total fields
|
|
763
|
+
</span>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
<div
|
|
767
|
+
class="space-y-3 rounded-xl bg-[var(--stor-surface-muted)] p-4"
|
|
768
|
+
>
|
|
769
|
+
<!-- Field Form -->
|
|
770
|
+
<div
|
|
771
|
+
class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end"
|
|
772
|
+
>
|
|
773
|
+
<div class="md:col-span-3 space-y-1">
|
|
774
|
+
<label
|
|
775
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
776
|
+
>Field Name</label
|
|
777
|
+
>
|
|
778
|
+
<input
|
|
779
|
+
type="text"
|
|
780
|
+
placeholder="title"
|
|
781
|
+
bind:value={editingField.name}
|
|
782
|
+
class="stor-native-field"
|
|
783
|
+
/>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="md:col-span-2 space-y-1">
|
|
786
|
+
<label
|
|
787
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
788
|
+
>Type</label
|
|
789
|
+
>
|
|
790
|
+
<select
|
|
791
|
+
bind:value={editingField.type}
|
|
792
|
+
class="stor-native-field"
|
|
793
|
+
>
|
|
794
|
+
<option value="string">String</option>
|
|
795
|
+
<option value="number">Number</option>
|
|
796
|
+
<option value="boolean">Boolean</option>
|
|
797
|
+
<option value="datetime">Datetime</option>
|
|
798
|
+
<option value="reference">Reference</option>
|
|
799
|
+
</select>
|
|
800
|
+
</div>
|
|
801
|
+
{#if editingField.type === "reference"}
|
|
802
|
+
<div class="md:col-span-3 space-y-1">
|
|
803
|
+
<label
|
|
804
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
805
|
+
>References</label
|
|
806
|
+
>
|
|
807
|
+
<select
|
|
808
|
+
bind:value={editingField.referenceTo}
|
|
809
|
+
class="stor-native-field"
|
|
810
|
+
>
|
|
811
|
+
<option value="">Select entity...</option>
|
|
812
|
+
{#each entityTypes as et}
|
|
813
|
+
<option value={et.name}
|
|
814
|
+
>{et.display_name}</option
|
|
815
|
+
>
|
|
816
|
+
{/each}
|
|
817
|
+
</select>
|
|
818
|
+
</div>
|
|
819
|
+
<div class="md:col-span-2 space-y-1">
|
|
820
|
+
<label
|
|
821
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
822
|
+
>Default</label
|
|
823
|
+
>
|
|
824
|
+
<input
|
|
825
|
+
type="text"
|
|
826
|
+
placeholder="optional"
|
|
827
|
+
bind:value={editingField.default}
|
|
828
|
+
class="stor-native-field"
|
|
829
|
+
/>
|
|
830
|
+
</div>
|
|
831
|
+
{:else if editingField.type === "datetime"}
|
|
832
|
+
<div class="md:col-span-3 space-y-1">
|
|
833
|
+
<label
|
|
834
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
835
|
+
>Default</label
|
|
836
|
+
>
|
|
837
|
+
<div
|
|
838
|
+
class="flex items-center gap-2 px-3 py-2 bg-[var(--stor-surface)] border border-[var(--stor-border)] rounded-lg cursor-pointer hover:bg-[var(--stor-surface-muted)] transition-colors whitespace-nowrap"
|
|
839
|
+
>
|
|
840
|
+
<Checkbox
|
|
841
|
+
checked={editingField.default ===
|
|
842
|
+
"datetime('now')"}
|
|
843
|
+
onCheckedChange={(checked) =>
|
|
844
|
+
(editingField.default = checked
|
|
845
|
+
? "datetime('now')"
|
|
846
|
+
: undefined)}
|
|
847
|
+
class="h-4 w-4 rounded"
|
|
848
|
+
/>
|
|
849
|
+
<span class="text-sm text-[var(--stor-muted-text)]"
|
|
850
|
+
>now()</span
|
|
851
|
+
>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
{:else}
|
|
855
|
+
<div class="md:col-span-3 space-y-1">
|
|
856
|
+
<label
|
|
857
|
+
class="text-[10.5px] font-bold text-[var(--stor-muted-text)] uppercase ml-1"
|
|
858
|
+
>Default</label
|
|
859
|
+
>
|
|
860
|
+
<input
|
|
861
|
+
type="text"
|
|
862
|
+
placeholder="optional"
|
|
863
|
+
bind:value={editingField.default}
|
|
864
|
+
class="stor-native-field"
|
|
865
|
+
/>
|
|
866
|
+
</div>
|
|
867
|
+
{/if}
|
|
868
|
+
<div class="md:col-span-2">
|
|
869
|
+
<Button onclick={addField} class="w-full">
|
|
870
|
+
{editingFieldIndex !== null ? "Update" : "Add"}
|
|
871
|
+
</Button>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
|
|
875
|
+
<!-- Fields List -->
|
|
876
|
+
{#if newFields.length > 0}
|
|
877
|
+
<div class="mt-6 space-y-2">
|
|
878
|
+
{#each newFields as field, index}
|
|
879
|
+
<div
|
|
880
|
+
class="flex items-center gap-3 p-3 bg-[var(--stor-surface)] border border-[var(--stor-border)] rounded-lg group/item"
|
|
881
|
+
in:fly={{ x: -10, duration: 200 }}
|
|
882
|
+
>
|
|
883
|
+
<div
|
|
884
|
+
class="w-8 h-8 bg-[var(--stor-surface-muted)] rounded-lg flex items-center justify-center text-xs font-bold text-[var(--stor-muted-text)]"
|
|
885
|
+
>
|
|
886
|
+
{index + 1}
|
|
887
|
+
</div>
|
|
888
|
+
<div class="flex-1">
|
|
889
|
+
<div class="flex items-center gap-2">
|
|
890
|
+
<span
|
|
891
|
+
class="font-semibold text-sm text-[var(--stor-ink)]"
|
|
892
|
+
>{field.name}</span
|
|
893
|
+
>
|
|
894
|
+
<span
|
|
895
|
+
class="text-[10.5px] bg-[var(--stor-surface-muted)] text-[var(--stor-muted-text)] px-1.5 py-0.5 rounded font-mono uppercase"
|
|
896
|
+
>{field.type}</span
|
|
897
|
+
>
|
|
898
|
+
{#if field.referenceTo}
|
|
899
|
+
<span
|
|
900
|
+
class="text-[10.5px] text-[var(--stor-accent-strong)]"
|
|
901
|
+
>→ {field.referenceTo}</span
|
|
902
|
+
>
|
|
903
|
+
{/if}
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
<div
|
|
907
|
+
class="flex items-center gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover/item:opacity-100 sm:group-focus-within/item:opacity-100"
|
|
908
|
+
>
|
|
909
|
+
<button
|
|
910
|
+
onclick={() => editField(index)}
|
|
911
|
+
class="p-1.5 text-[var(--stor-muted-text)] hover:text-[var(--stor-ink)] hover:bg-[var(--stor-surface-muted)] rounded-lg transition-all"
|
|
912
|
+
>
|
|
913
|
+
<svg
|
|
914
|
+
class="w-4 h-4"
|
|
915
|
+
fill="none"
|
|
916
|
+
stroke="currentColor"
|
|
917
|
+
viewBox="0 0 24 24"
|
|
918
|
+
>
|
|
919
|
+
<path
|
|
920
|
+
stroke-linecap="round"
|
|
921
|
+
stroke-linejoin="round"
|
|
922
|
+
stroke-width="2"
|
|
923
|
+
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
924
|
+
/>
|
|
925
|
+
</svg>
|
|
926
|
+
</button>
|
|
927
|
+
<button
|
|
928
|
+
onclick={() => removeField(index)}
|
|
929
|
+
class="p-1.5 text-[var(--stor-muted-text)] hover:text-[var(--stor-negative)] hover:bg-[var(--stor-negative-soft)] rounded-lg transition-all"
|
|
930
|
+
>
|
|
931
|
+
<svg
|
|
932
|
+
class="w-4 h-4"
|
|
933
|
+
fill="none"
|
|
934
|
+
stroke="currentColor"
|
|
935
|
+
viewBox="0 0 24 24"
|
|
936
|
+
>
|
|
937
|
+
<path
|
|
938
|
+
stroke-linecap="round"
|
|
939
|
+
stroke-linejoin="round"
|
|
940
|
+
stroke-width="2"
|
|
941
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
942
|
+
/>
|
|
943
|
+
</svg>
|
|
944
|
+
</button>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
{/each}
|
|
948
|
+
</div>
|
|
949
|
+
{/if}
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<CalendarSourceFields
|
|
954
|
+
fields={newFields}
|
|
955
|
+
bind:draft={calendarDraft}
|
|
956
|
+
defaultLabel={newDisplayName}
|
|
957
|
+
/>
|
|
958
|
+
|
|
959
|
+
</div>
|
|
960
|
+
{/if}
|
|
961
|
+
{#snippet footer()}
|
|
962
|
+
<Button variant="secondary" onclick={cancelEdit}>Cancel</Button>
|
|
963
|
+
<Button
|
|
964
|
+
onclick={handleRedefinition}
|
|
965
|
+
disabled={newFields.length === 0 || calendarDraftInvalid}
|
|
966
|
+
>
|
|
967
|
+
<svg
|
|
968
|
+
class="w-5 h-5"
|
|
969
|
+
fill="none"
|
|
970
|
+
stroke="currentColor"
|
|
971
|
+
viewBox="0 0 24 24"
|
|
972
|
+
>
|
|
973
|
+
<path
|
|
974
|
+
stroke-linecap="round"
|
|
975
|
+
stroke-linejoin="round"
|
|
976
|
+
stroke-width="2"
|
|
977
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
978
|
+
/>
|
|
979
|
+
</svg>
|
|
980
|
+
Save & Reset Data
|
|
981
|
+
</Button>
|
|
982
|
+
|
|
983
|
+
{/snippet}
|
|
984
|
+
</CommandDrawer>
|
|
985
|
+
|
|
986
|
+
<!-- Export Modal -->
|
|
987
|
+
<Modal
|
|
988
|
+
show={showExportModal}
|
|
989
|
+
title="Code Export"
|
|
990
|
+
onClose={() => (showExportModal = false)}
|
|
991
|
+
>
|
|
992
|
+
<div class="space-y-6">
|
|
993
|
+
<div class="bg-stone-900 rounded-xl p-6 relative group">
|
|
994
|
+
<pre
|
|
995
|
+
class="text-stone-300 text-sm overflow-x-auto font-mono leading-relaxed"><code
|
|
996
|
+
>{exportCode}</code
|
|
997
|
+
></pre>
|
|
998
|
+
<button
|
|
999
|
+
onclick={copyToClipboard}
|
|
1000
|
+
class="absolute top-4 right-4 px-4 py-2 bg-white/10 hover:bg-white/20 text-white text-xs font-bold rounded-xl backdrop-blur-md transition-all opacity-0 group-hover:opacity-100"
|
|
1001
|
+
>
|
|
1002
|
+
Copy Snippet
|
|
1003
|
+
</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div class="rounded-lg bg-[var(--stor-surface-muted)] p-4 flex gap-3 text-[var(--stor-info)]">
|
|
1007
|
+
<svg
|
|
1008
|
+
class="w-5 h-5 flex-shrink-0"
|
|
1009
|
+
fill="none"
|
|
1010
|
+
stroke="currentColor"
|
|
1011
|
+
viewBox="0 0 24 24"
|
|
1012
|
+
>
|
|
1013
|
+
<path
|
|
1014
|
+
stroke-linecap="round"
|
|
1015
|
+
stroke-linejoin="round"
|
|
1016
|
+
stroke-width="2"
|
|
1017
|
+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1018
|
+
/>
|
|
1019
|
+
</svg>
|
|
1020
|
+
<p class="text-xs font-medium leading-relaxed">
|
|
1021
|
+
Copy this snippet into your <code>entity-definitions.ts</code> file
|
|
1022
|
+
to convert this dynamic type into a fully-typed static entity with
|
|
1023
|
+
auto-generated code.
|
|
1024
|
+
</p>
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
<div class="flex justify-end pt-4">
|
|
1028
|
+
<button
|
|
1029
|
+
onclick={() => (showExportModal = false)}
|
|
1030
|
+
class="rounded-xl bg-[var(--stor-action)] px-8 py-3 font-semibold text-[var(--stor-action-text)] transition-all hover:bg-[var(--stor-action-hover)]"
|
|
1031
|
+
>
|
|
1032
|
+
Done
|
|
1033
|
+
</button>
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
</Modal>
|
|
1037
|
+
|
|
1038
|
+
<CommandConfirmDialog
|
|
1039
|
+
show={showArchiveConfirm}
|
|
1040
|
+
title="Archive Entity Type"
|
|
1041
|
+
message={`Archive "${itemToArchive?.display_name}"? The data will be preserved but the entity type will be hidden.`}
|
|
1042
|
+
onConfirm={executeArchive}
|
|
1043
|
+
onCancel={() => (showArchiveConfirm = false)}
|
|
1044
|
+
/>
|
|
1045
|
+
|
|
1046
|
+
<CommandConfirmDialog
|
|
1047
|
+
show={showRedefineConfirm}
|
|
1048
|
+
title="Confirm Redefinition"
|
|
1049
|
+
message={`⚠️ WARNING: This will DELETE ALL existing ${isEditing?.display_name} data!\n\nThe entity type will be redefined with the new schema, and all existing records will be permanently removed.\n\nAre you sure you want to continue?`}
|
|
1050
|
+
onConfirm={executeRedefinition}
|
|
1051
|
+
onCancel={() => (showRedefineConfirm = false)}
|
|
1052
|
+
/>
|
|
1053
|
+
|
|
1054
|
+
<style>
|
|
1055
|
+
/* Simple custom scrollbar for better look */
|
|
1056
|
+
::-webkit-scrollbar {
|
|
1057
|
+
width: 8px;
|
|
1058
|
+
}
|
|
1059
|
+
::-webkit-scrollbar-track {
|
|
1060
|
+
background: transparent;
|
|
1061
|
+
}
|
|
1062
|
+
::-webkit-scrollbar-thumb {
|
|
1063
|
+
background: #e7e5e4;
|
|
1064
|
+
border-radius: 10px;
|
|
1065
|
+
}
|
|
1066
|
+
::-webkit-scrollbar-thumb:hover {
|
|
1067
|
+
background: #d6d3d1;
|
|
1068
|
+
}
|
|
1069
|
+
</style>
|