@agent-native/core-corpus 0.213.1 → 0.214.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 (22) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/community/qr/package.json +1 -1
  3. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
  4. package/corpus/templates/content/CHANGELOG.md +5 -0
  5. package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
  6. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  7. package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
  8. package/corpus/templates/content/actions/view-screen.ts +12 -0
  9. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +91 -16
  10. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  11. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  12. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  13. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  14. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  15. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  16. package/corpus/templates/content/app/global.css +13 -0
  17. package/corpus/templates/content/app/i18n-data.ts +32 -0
  18. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  19. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  20. package/corpus/templates/content/shared/api.ts +18 -0
  21. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  22. package/package.json +1 -1
@@ -0,0 +1,426 @@
1
+ import type {
2
+ ContentDatabaseItem,
3
+ ContentDatabaseItemLayout,
4
+ DocumentProperty,
5
+ } from "@shared/api";
6
+ import type {
7
+ ContentDatabaseItemLayoutSlot,
8
+ ResolvedContentDatabaseItemLayout,
9
+ } from "@shared/database-item-layout";
10
+ import { stripMarkdownForPreview } from "@shared/og-description";
11
+ import {
12
+ isEmptyPropertyValue,
13
+ type DocumentPropertyOptionColor,
14
+ } from "@shared/properties";
15
+ import { IconCheck } from "@tabler/icons-react";
16
+ import type { ReactNode } from "react";
17
+
18
+ import {
19
+ Popover,
20
+ PopoverContent,
21
+ PopoverTrigger,
22
+ } from "@/components/ui/popover";
23
+ import { cn } from "@/lib/utils";
24
+
25
+ import { displayValue, PropertyValuePopover } from "../DocumentProperties";
26
+ import { DatabaseItemPageIcon, dbText } from "./DatabaseView";
27
+ import {
28
+ ListItemLayoutSlotEditor,
29
+ listItemLayoutSlotLabel,
30
+ } from "./ListItemLayout";
31
+
32
+ export interface ListItemRowLayout {
33
+ /** The layout being edited: the saved one or the default rows' equivalent. */
34
+ layout: ContentDatabaseItemLayout;
35
+ resolved: ResolvedContentDatabaseItemLayout<DocumentProperty>;
36
+ properties: DocumentProperty[];
37
+ editing: boolean;
38
+ onLayoutChange: (layout: ContentDatabaseItemLayout) => void;
39
+ onToggleCheckbox: (
40
+ item: ContentDatabaseItem,
41
+ property: DocumentProperty,
42
+ ) => void;
43
+ }
44
+
45
+ const STATUS_DOT_CLASSES: Record<DocumentPropertyOptionColor, string> = {
46
+ gray: "border-muted-foreground/50 bg-muted-foreground/10 dark:border-muted-foreground dark:bg-muted-foreground/20",
47
+ brown: "border-amber-700 bg-amber-700/25",
48
+ orange: "border-orange-500 bg-orange-500/25",
49
+ yellow: "border-yellow-500 bg-yellow-500/30",
50
+ green: "border-emerald-500 bg-emerald-500",
51
+ blue: "border-sky-500 bg-sky-500/30",
52
+ purple: "border-violet-500 bg-violet-500/30",
53
+ pink: "border-pink-500 bg-pink-500/30",
54
+ red: "border-rose-500 bg-rose-500/30",
55
+ };
56
+
57
+ const LINE_CLAMP_CLASSES = {
58
+ 1: "line-clamp-1",
59
+ 2: "line-clamp-2",
60
+ 3: "line-clamp-3",
61
+ } as const;
62
+
63
+ const TITLE_WEIGHT_CLASSES = {
64
+ regular: "font-normal",
65
+ medium: "font-medium",
66
+ bold: "font-semibold",
67
+ } as const;
68
+
69
+ function itemValue(item: ContentDatabaseItem, property: DocumentProperty) {
70
+ return (
71
+ item.properties.find(
72
+ (candidate) => candidate.definition.id === property.definition.id,
73
+ ) ?? { ...property, value: null }
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Values a row leaves out entirely, so empty slots take no space. An
79
+ * unchecked box is a checkbox's empty value; a checked one shows by name.
80
+ */
81
+ function isBlankValue(property: DocumentProperty) {
82
+ return (
83
+ isEmptyPropertyValue(property.value) ||
84
+ (property.definition.type === "checkbox" && property.value !== true)
85
+ );
86
+ }
87
+
88
+ function line2Text(property: DocumentProperty) {
89
+ const value = property.value;
90
+ if (typeof value !== "string") return "";
91
+ return property.definition.type === "blocks"
92
+ ? stripMarkdownForPreview(value)
93
+ : value.trim();
94
+ }
95
+
96
+ /** The fields a slot shows, which can be set but blank on a given row. */
97
+ function slotFieldNames(
98
+ resolved: ResolvedContentDatabaseItemLayout<DocumentProperty>,
99
+ slot: ContentDatabaseItemLayoutSlot,
100
+ ) {
101
+ const fields =
102
+ slot === "line2"
103
+ ? resolved.line2.property
104
+ ? [resolved.line2.property]
105
+ : []
106
+ : slot === "line3" || slot === "trailing"
107
+ ? resolved[slot]
108
+ : [];
109
+ return fields.map((property) => property.definition.name);
110
+ }
111
+
112
+ function SlotZone({
113
+ slot,
114
+ row,
115
+ showPlaceholder,
116
+ empty,
117
+ className,
118
+ children,
119
+ }: {
120
+ slot: ContentDatabaseItemLayoutSlot;
121
+ row: ListItemRowLayout;
122
+ showPlaceholder: boolean;
123
+ empty: boolean;
124
+ className?: string;
125
+ children: ReactNode;
126
+ }) {
127
+ if (!row.editing) return empty ? null : <>{children}</>;
128
+ if (empty && !showPlaceholder) return null;
129
+ const label = listItemLayoutSlotLabel(slot);
130
+ const fields = slotFieldNames(row.resolved, slot);
131
+ const unset = empty && fields.length === 0;
132
+ const action = dbText(unset ? "listRowsAddSlot" : "listRowsChangeSlot", {
133
+ slot: label,
134
+ });
135
+ return (
136
+ <Popover>
137
+ <PopoverTrigger asChild>
138
+ <button
139
+ type="button"
140
+ aria-label={unset ? action : undefined}
141
+ className={cn(
142
+ "rounded-md border border-dashed border-database-blue/50 text-left transition-colors hover:border-database-blue hover:bg-database-blue/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:border-database-blue data-[state=open]:bg-database-blue/10",
143
+ empty ? "px-2 py-0.5 text-xs text-database-blue/80" : "px-1 py-0.5",
144
+ className,
145
+ )}
146
+ >
147
+ {unset ? (
148
+ `+ ${label}`
149
+ ) : (
150
+ <>
151
+ {/* Screen readers hear the action, then what the slot shows. */}
152
+ <span className="sr-only">{action}: </span>
153
+ {empty ? (
154
+ <>
155
+ {label}
156
+ <span className="ml-1 text-muted-foreground">
157
+ {fields.join(", ")}
158
+ </span>
159
+ </>
160
+ ) : (
161
+ children
162
+ )}
163
+ </>
164
+ )}
165
+ </button>
166
+ </PopoverTrigger>
167
+ <PopoverContent align="start">
168
+ <div className="mb-2 text-xs font-medium text-muted-foreground">
169
+ {label}
170
+ </div>
171
+ <ListItemLayoutSlotEditor
172
+ slot={slot}
173
+ layout={row.layout}
174
+ properties={row.properties}
175
+ onChange={row.onLayoutChange}
176
+ />
177
+ </PopoverContent>
178
+ </Popover>
179
+ );
180
+ }
181
+
182
+ function MetaValues({ values }: { values: DocumentProperty[] }) {
183
+ return values.map((property) => (
184
+ <span
185
+ key={property.definition.id}
186
+ className="inline-flex max-w-48 min-w-0 items-center truncate"
187
+ >
188
+ {property.definition.type === "checkbox" ? (
189
+ <span className="inline-flex min-w-0 items-center gap-1 text-foreground">
190
+ <IconCheck className="size-3.5 shrink-0" />
191
+ <span className="truncate">{property.definition.name}</span>
192
+ </span>
193
+ ) : (
194
+ displayValue(property)
195
+ )}
196
+ </span>
197
+ ));
198
+ }
199
+
200
+ /** A list row whose parts follow the view's item layout. */
201
+ export function DatabaseLayoutListRow({
202
+ row,
203
+ isFirst,
204
+ item,
205
+ databaseDocumentId,
206
+ canEdit,
207
+ onPreview,
208
+ actions,
209
+ }: {
210
+ row: ListItemRowLayout;
211
+ isFirst: boolean;
212
+ item: ContentDatabaseItem;
213
+ databaseDocumentId: string;
214
+ canEdit: boolean;
215
+ onPreview: () => void;
216
+ actions: ReactNode;
217
+ }) {
218
+ const { resolved, editing } = row;
219
+ const title = item.document.title || "Untitled";
220
+ const leadingProperty = resolved.leading.property
221
+ ? itemValue(item, resolved.leading.property)
222
+ : null;
223
+ const checked =
224
+ resolved.leading.kind === "checkbox" && leadingProperty?.value === true;
225
+ const interactive = canEdit && !editing && leadingProperty?.editable === true;
226
+
227
+ let leading: ReactNode = null;
228
+ if (resolved.leading.kind === "page_icon") {
229
+ leading = (
230
+ <DatabaseItemPageIcon
231
+ document={item.document}
232
+ className="size-4 text-sm"
233
+ fallbackClassName="size-4"
234
+ />
235
+ );
236
+ } else if (resolved.leading.kind === "checkbox" && leadingProperty) {
237
+ const box = cn(
238
+ "flex size-4 items-center justify-center rounded border-2 transition-colors",
239
+ checked
240
+ ? "border-database-blue bg-database-blue text-database-blue-foreground"
241
+ : "border-muted-foreground/60",
242
+ );
243
+ const mark = checked ? (
244
+ <IconCheck aria-hidden="true" className="size-3" stroke={3} />
245
+ ) : null;
246
+ // In Edit rows the whole slot is a button, so the box is only a picture,
247
+ // with its field and state as text for screen readers.
248
+ leading = editing ? (
249
+ <span className={box}>
250
+ {mark}
251
+ <span className="sr-only">
252
+ {`${leadingProperty.definition.name}: ${dbText(checked ? "checked" : "unchecked")}`}
253
+ </span>
254
+ </span>
255
+ ) : (
256
+ <button
257
+ type="button"
258
+ role="checkbox"
259
+ aria-checked={checked}
260
+ aria-label={`${leadingProperty.definition.name}: ${title}`}
261
+ disabled={!interactive}
262
+ className={cn(
263
+ box,
264
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default",
265
+ !checked && "enabled:hover:border-foreground",
266
+ )}
267
+ onClick={() => row.onToggleCheckbox(item, leadingProperty)}
268
+ >
269
+ {mark}
270
+ </button>
271
+ );
272
+ } else if (resolved.leading.kind === "status" && leadingProperty) {
273
+ const option = (leadingProperty.definition.options.options ?? []).find(
274
+ (candidate) => candidate.id === leadingProperty.value,
275
+ );
276
+ const statusName = option?.name ?? dbText("listRowsNoStatus");
277
+ const dot = (
278
+ <span
279
+ title={statusName}
280
+ className={cn(
281
+ "block size-3.5 shrink-0 rounded-full border-2",
282
+ option
283
+ ? STATUS_DOT_CLASSES[option.color]
284
+ : "border-dashed border-muted-foreground/60",
285
+ )}
286
+ >
287
+ <span className="sr-only">{statusName}</span>
288
+ </span>
289
+ );
290
+ leading = interactive ? (
291
+ <span className="flex w-6 justify-center">
292
+ <PropertyValuePopover
293
+ property={leadingProperty}
294
+ documentId={item.document.id}
295
+ databaseDocumentId={databaseDocumentId}
296
+ >
297
+ {dot}
298
+ </PropertyValuePopover>
299
+ </span>
300
+ ) : (
301
+ dot
302
+ );
303
+ }
304
+
305
+ const line2Property = resolved.line2.property
306
+ ? itemValue(item, resolved.line2.property)
307
+ : null;
308
+ const line2 = line2Property ? line2Text(line2Property) : "";
309
+ const filled = (properties: DocumentProperty[]) =>
310
+ properties
311
+ .map((property) => itemValue(item, property))
312
+ .filter((property) => !isBlankValue(property));
313
+ const line3 = filled(resolved.line3);
314
+ const trailing = filled(resolved.trailing);
315
+
316
+ const body = (
317
+ <>
318
+ <SlotZone
319
+ slot="title"
320
+ row={row}
321
+ showPlaceholder={false}
322
+ empty={false}
323
+ className="block max-w-full"
324
+ >
325
+ <span
326
+ className={cn(
327
+ "block min-w-0 truncate text-sm",
328
+ TITLE_WEIGHT_CLASSES[resolved.titleWeight],
329
+ checked &&
330
+ "text-muted-foreground line-through decoration-muted-foreground/70",
331
+ )}
332
+ >
333
+ {title}
334
+ </span>
335
+ </SlotZone>
336
+ <SlotZone
337
+ slot="line2"
338
+ row={row}
339
+ showPlaceholder={isFirst}
340
+ empty={!line2}
341
+ className="mt-0.5 block w-full"
342
+ >
343
+ <span
344
+ className={cn(
345
+ "text-xs leading-relaxed whitespace-pre-line text-muted-foreground",
346
+ LINE_CLAMP_CLASSES[resolved.line2.lines],
347
+ )}
348
+ >
349
+ {line2}
350
+ </span>
351
+ </SlotZone>
352
+ <SlotZone
353
+ slot="line3"
354
+ row={row}
355
+ showPlaceholder={isFirst}
356
+ empty={line3.length === 0}
357
+ className="mt-1 inline-flex max-w-full"
358
+ >
359
+ <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
360
+ <MetaValues values={line3} />
361
+ </span>
362
+ </SlotZone>
363
+ {/* On phones End sits under the title, so it's edited there too. */}
364
+ <SlotZone
365
+ slot="trailing"
366
+ row={row}
367
+ showPlaceholder={isFirst}
368
+ empty={trailing.length === 0}
369
+ className="mt-1 inline-flex max-w-full sm:hidden"
370
+ >
371
+ <span
372
+ className={cn(
373
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground",
374
+ !editing && "mt-1 sm:hidden",
375
+ )}
376
+ >
377
+ <MetaValues values={trailing} />
378
+ </span>
379
+ </SlotZone>
380
+ </>
381
+ );
382
+
383
+ return (
384
+ <div
385
+ className={cn(
386
+ "group flex min-h-10 items-start gap-2 border-t border-border px-1 py-1.5",
387
+ !editing && "hover:bg-muted/40",
388
+ )}
389
+ >
390
+ <SlotZone
391
+ slot="leading"
392
+ row={row}
393
+ showPlaceholder={isFirst}
394
+ empty={!leading}
395
+ className="mt-1 shrink-0"
396
+ >
397
+ <span className="flex min-w-5 h-5 items-center justify-center">
398
+ {leading}
399
+ </span>
400
+ </SlotZone>
401
+ {editing ? (
402
+ <div className="min-w-0 flex-1 px-1.5 py-0.5">{body}</div>
403
+ ) : (
404
+ <button
405
+ type="button"
406
+ className="block min-w-0 flex-1 rounded px-1.5 py-0.5 text-left hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
407
+ onClick={onPreview}
408
+ >
409
+ {body}
410
+ </button>
411
+ )}
412
+ <SlotZone
413
+ slot="trailing"
414
+ row={row}
415
+ showPlaceholder={isFirst}
416
+ empty={trailing.length === 0}
417
+ className="mt-0.5 hidden shrink-0 sm:inline-flex"
418
+ >
419
+ <span className="mt-1 hidden max-w-xs min-w-0 flex-wrap items-center justify-end gap-2 text-xs text-muted-foreground sm:flex">
420
+ <MetaValues values={trailing} />
421
+ </span>
422
+ </SlotZone>
423
+ {editing ? null : actions}
424
+ </div>
425
+ );
426
+ }
@@ -1,8 +1,19 @@
1
- import type { ContentDatabaseItem, DocumentProperty } from "@shared/api";
2
- import { IconList, IconPlus } from "@tabler/icons-react";
1
+ import type {
2
+ ContentDatabaseItem,
3
+ ContentDatabaseItemLayout,
4
+ DocumentProperty,
5
+ } from "@shared/api";
6
+ import {
7
+ resolveContentDatabaseItemLayout,
8
+ startingContentDatabaseItemLayout,
9
+ } from "@shared/database-item-layout";
10
+ import { IconCheck, IconList, IconPencil, IconPlus } from "@tabler/icons-react";
3
11
  import { useRef, useState } from "react";
12
+ import { toast } from "sonner";
4
13
 
5
14
  import { Spinner } from "@/components/ui/spinner";
15
+ import { useSetDocumentProperty } from "@/hooks/use-document-properties";
16
+ import { cn } from "@/lib/utils";
6
17
 
7
18
  import { displayValue } from "../DocumentProperties";
8
19
  import {
@@ -20,12 +31,18 @@ import {
20
31
  type DatabaseBoardGroup,
21
32
  type DatabaseFilter,
22
33
  } from "./DatabaseView";
34
+ import { ListItemLayoutPresets } from "./ListItemLayout";
35
+ import { DatabaseLayoutListRow, type ListItemRowLayout } from "./ListItemRow";
23
36
 
24
37
  export function DatabaseListView({
25
38
  properties,
26
39
  groupableProperties,
40
+ layoutProperties,
27
41
  items,
42
+ databaseId,
28
43
  databaseDocumentId,
44
+ itemLayout,
45
+ editingRows,
29
46
  canEdit,
30
47
  canCreateItems,
31
48
  isLoading,
@@ -43,11 +60,18 @@ export function DatabaseListView({
43
60
  onPreview,
44
61
  onDeletedPreviewItem,
45
62
  onOpenPage,
63
+ onItemLayoutChange,
64
+ onEditingRowsChange,
46
65
  }: {
47
66
  properties: DocumentProperty[];
48
67
  groupableProperties: DocumentProperty[];
68
+ /** What custom rows can show: properties not hidden in this view. */
69
+ layoutProperties: DocumentProperty[];
49
70
  items: ContentDatabaseItem[];
71
+ databaseId: string;
50
72
  databaseDocumentId: string;
73
+ itemLayout: ContentDatabaseItemLayout | null;
74
+ editingRows: boolean;
51
75
  canEdit: boolean;
52
76
  canCreateItems: boolean;
53
77
  isLoading: boolean;
@@ -68,7 +92,44 @@ export function DatabaseListView({
68
92
  onPreview: (item: ContentDatabaseItem) => void;
69
93
  onDeletedPreviewItem: (item: ContentDatabaseItem) => boolean;
70
94
  onOpenPage: (item: ContentDatabaseItem) => void;
95
+ onItemLayoutChange: (layout: ContentDatabaseItemLayout | null) => void;
96
+ onEditingRowsChange: (editing: boolean) => void;
71
97
  }) {
98
+ const setProperty = useSetDocumentProperty(
99
+ databaseDocumentId,
100
+ databaseId,
101
+ databaseDocumentId,
102
+ { errorNotification: "caller" },
103
+ );
104
+ const editing = canEdit && editingRows;
105
+ const layout =
106
+ itemLayout ??
107
+ (editing ? startingContentDatabaseItemLayout(properties) : null);
108
+ const layoutRow: ListItemRowLayout | null = layout
109
+ ? {
110
+ layout,
111
+ resolved: resolveContentDatabaseItemLayout(layout, layoutProperties),
112
+ properties: layoutProperties,
113
+ editing,
114
+ onLayoutChange: onItemLayoutChange,
115
+ onToggleCheckbox: (item, property) => {
116
+ setProperty
117
+ .mutateAsync({
118
+ documentId: item.document.id,
119
+ propertyId: property.definition.id,
120
+ value: property.value !== true,
121
+ })
122
+ .catch((err: unknown) => {
123
+ toast.error(dbText("failedToUpdateCheckbox"), {
124
+ description:
125
+ err instanceof Error
126
+ ? err.message
127
+ : dbText("somethingWentWrong"),
128
+ });
129
+ });
130
+ },
131
+ }
132
+ : null;
72
133
  const groups = databaseVisibleGroups(
73
134
  databaseViewItemGroups(items, groupableProperties, groupByPropertyId),
74
135
  hideEmptyGroups,
@@ -83,14 +144,44 @@ export function DatabaseListView({
83
144
  <div className="flex min-h-9 items-center gap-2 border-t border-border px-1 text-xs text-muted-foreground">
84
145
  <IconList className="size-4 shrink-0" />
85
146
  <span>List</span>
147
+ {canEdit ? (
148
+ <button
149
+ type="button"
150
+ aria-pressed={editing}
151
+ className={cn(
152
+ "ml-auto flex h-7 items-center gap-1.5 rounded-md px-2 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
153
+ editing
154
+ ? "bg-database-blue text-database-blue-foreground"
155
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
156
+ )}
157
+ onClick={() => onEditingRowsChange(!editing)}
158
+ >
159
+ {editing ? (
160
+ <IconCheck className="size-3.5" />
161
+ ) : (
162
+ <IconPencil className="size-3.5" />
163
+ )}
164
+ {dbText(editing ? "listRowsDone" : "listRowsEdit")}
165
+ </button>
166
+ ) : null}
86
167
  </div>
168
+ {editing ? (
169
+ <div className="flex flex-wrap items-center gap-2 border-t border-dashed border-database-blue/40 bg-database-blue/5 px-2 py-2 text-xs text-muted-foreground">
170
+ <ListItemLayoutPresets
171
+ layout={itemLayout}
172
+ properties={layoutProperties}
173
+ onChange={onItemLayoutChange}
174
+ />
175
+ <span>{dbText("listRowsEditHint")}</span>
176
+ </div>
177
+ ) : null}
87
178
  {isLoading ? (
88
179
  <div className="flex h-16 items-center gap-2 px-2 text-sm text-muted-foreground">
89
180
  <Spinner className="size-4" />
90
181
  {dbText("loadingList")}
91
182
  </div>
92
183
  ) : (
93
- <div className="grid">
184
+ <div className="grid grid-cols-1">
94
185
  {databaseViewHasNoMatchingPages(
95
186
  items.length,
96
187
  hasSearch,
@@ -102,6 +193,7 @@ export function DatabaseListView({
102
193
  ? groups.map((group) => (
103
194
  <DatabaseGroupedListSection
104
195
  key={group.id}
196
+ layoutRow={layoutRow}
105
197
  group={group}
106
198
  properties={properties}
107
199
  databaseDocumentId={databaseDocumentId}
@@ -124,6 +216,8 @@ export function DatabaseListView({
124
216
  : items.map((item, index) => (
125
217
  <DatabaseListRow
126
218
  key={item.id}
219
+ layoutRow={layoutRow}
220
+ isFirst={index === 0}
127
221
  item={item}
128
222
  properties={properties}
129
223
  databaseDocumentId={databaseDocumentId}
@@ -140,7 +234,7 @@ export function DatabaseListView({
140
234
  onOpenPage={() => onOpenPage(item)}
141
235
  />
142
236
  ))}
143
- {canEdit && canCreateItems && !grouped ? (
237
+ {canEdit && canCreateItems && !grouped && !editing ? (
144
238
  <NewListRow
145
239
  disabled={isCreating}
146
240
  isPending={isCreating}
@@ -154,6 +248,7 @@ export function DatabaseListView({
154
248
  }
155
249
 
156
250
  function DatabaseGroupedListSection({
251
+ layoutRow,
157
252
  group,
158
253
  properties,
159
254
  databaseDocumentId,
@@ -167,6 +262,7 @@ function DatabaseGroupedListSection({
167
262
  onDeletedPreviewItem,
168
263
  onOpenPage,
169
264
  }: {
265
+ layoutRow: ListItemRowLayout | null;
170
266
  group: DatabaseBoardGroup;
171
267
  properties: DocumentProperty[];
172
268
  databaseDocumentId: string;
@@ -195,6 +291,8 @@ function DatabaseGroupedListSection({
195
291
  {group.items.map((item, index) => (
196
292
  <DatabaseListRow
197
293
  key={`${group.id}-${item.id}`}
294
+ layoutRow={layoutRow}
295
+ isFirst={index === 0}
198
296
  item={item}
199
297
  properties={properties}
200
298
  databaseDocumentId={databaseDocumentId}
@@ -209,7 +307,7 @@ function DatabaseGroupedListSection({
209
307
  onOpenPage={() => onOpenPage(item)}
210
308
  />
211
309
  ))}
212
- {canEdit && canCreateItems ? (
310
+ {canEdit && canCreateItems && !layoutRow?.editing ? (
213
311
  <NewListRow
214
312
  disabled={isCreating}
215
313
  isPending={isCreating}
@@ -223,6 +321,8 @@ function DatabaseGroupedListSection({
223
321
  }
224
322
 
225
323
  function DatabaseListRow({
324
+ layoutRow,
325
+ isFirst,
226
326
  item,
227
327
  properties,
228
328
  databaseDocumentId,
@@ -236,6 +336,8 @@ function DatabaseListRow({
236
336
  onPreview,
237
337
  onOpenPage,
238
338
  }: {
339
+ layoutRow: ListItemRowLayout | null;
340
+ isFirst: boolean;
239
341
  item: ContentDatabaseItem;
240
342
  properties: DocumentProperty[];
241
343
  databaseDocumentId: string;
@@ -249,6 +351,34 @@ function DatabaseListRow({
249
351
  onPreview: () => void;
250
352
  onOpenPage: () => void;
251
353
  }) {
354
+ const actions = canEdit ? (
355
+ <RowActionsCell
356
+ item={item}
357
+ databaseDocumentId={databaseDocumentId}
358
+ rowIndex={rowIndex}
359
+ canReorder={canReorder}
360
+ canMoveUp={canMoveUp}
361
+ canMoveDown={canMoveDown}
362
+ onPreviewItem={onPreviewItem}
363
+ onDeletedPreviewItem={onDeletedPreviewItem}
364
+ onOpenPage={onOpenPage}
365
+ />
366
+ ) : null;
367
+
368
+ if (layoutRow) {
369
+ return (
370
+ <DatabaseLayoutListRow
371
+ row={layoutRow}
372
+ isFirst={isFirst}
373
+ item={item}
374
+ databaseDocumentId={databaseDocumentId}
375
+ canEdit={canEdit}
376
+ onPreview={onPreview}
377
+ actions={actions}
378
+ />
379
+ );
380
+ }
381
+
252
382
  const visibleProperties = properties.slice(0, 4);
253
383
 
254
384
  return (
@@ -286,19 +416,7 @@ function DatabaseListRow({
286
416
  </span>
287
417
  ) : null}
288
418
  </button>
289
- {canEdit ? (
290
- <RowActionsCell
291
- item={item}
292
- databaseDocumentId={databaseDocumentId}
293
- rowIndex={rowIndex}
294
- canReorder={canReorder}
295
- canMoveUp={canMoveUp}
296
- canMoveDown={canMoveDown}
297
- onPreviewItem={onPreviewItem}
298
- onDeletedPreviewItem={onDeletedPreviewItem}
299
- onOpenPage={onOpenPage}
300
- />
301
- ) : null}
419
+ {actions}
302
420
  </div>
303
421
  );
304
422
  }