+ {Array.from({ length: 5 }, (_, index) => (
+
0 && cn('border-t', dashedRule))}>
+
+
+
+ ))}
+
+ );
+}
diff --git a/packages/ui/src/components/resource-kit/record-dialogs.tsx b/packages/ui/src/components/resource-kit/record-dialogs.tsx
new file mode 100644
index 00000000..2d90a84f
--- /dev/null
+++ b/packages/ui/src/components/resource-kit/record-dialogs.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import * as React from 'react';
+
+import { DeleteDialog } from './delete-dialog';
+import { EntityFormSheet } from './entity-form-sheet';
+import type { AnyResource, ResourceRow } from './types';
+
+/** A dialog keeps its record while it closes, so its copy does not blank out. */
+type DialogState = { open: boolean; record: ResourceRow | null };
+
+const CLOSED: DialogState = { open: false, record: null };
+
+type RecordDialogsOptions = {
+ /** Starting values for a new record, e.g. the parent of a related list. */
+ defaultValues?: ResourceRow;
+ onDeleted?: (id: string) => void;
+ /** The record a detail view shows: the dialogs follow its live row instead of the row they opened with. */
+ record?: ResourceRow | null;
+};
+
+/** The create and edit sheet and the delete confirmation of one resource, with the state that opens them. */
+export function useRecordDialogs(resource: AnyResource, { defaultValues, onDeleted, record }: RecordDialogsOptions = {}) {
+ const [sheet, setSheet] = React.useState