@agent-native/core 0.116.0 → 0.117.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.
Files changed (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +4 -1
  4. package/corpus/core/src/client/analytics.ts +7 -0
  5. package/corpus/core/src/client/use-db-sync.ts +354 -15
  6. package/corpus/core/src/collab/client.ts +26 -2
  7. package/corpus/core/src/db/ddl-guard.ts +49 -10
  8. package/corpus/core/src/deploy/build.ts +25 -2
  9. package/corpus/core/src/deploy/workspace-deploy.ts +14 -4
  10. package/corpus/core/src/realtime-protocol.ts +92 -0
  11. package/corpus/core/src/server/core-routes-plugin.ts +7 -0
  12. package/corpus/core/src/server/poll-events.ts +29 -8
  13. package/corpus/core/src/server/poll.ts +1123 -865
  14. package/corpus/core/src/server/realtime-token.ts +106 -0
  15. package/corpus/core/src/server/sentry-config.ts +40 -0
  16. package/corpus/core/src/server/short-lived-token.ts +156 -0
  17. package/corpus/core/src/server/sse.ts +15 -0
  18. package/corpus/core/src/server/ssr-handler.ts +9 -3
  19. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +12 -0
  20. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  21. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +566 -299
  22. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +14 -0
  23. package/corpus/templates/content/app/i18n-data.ts +90 -0
  24. package/dist/client/analytics.d.ts +10 -0
  25. package/dist/client/analytics.d.ts.map +1 -1
  26. package/dist/client/analytics.js.map +1 -1
  27. package/dist/client/use-db-sync.d.ts +1 -1
  28. package/dist/client/use-db-sync.d.ts.map +1 -1
  29. package/dist/client/use-db-sync.js +315 -15
  30. package/dist/client/use-db-sync.js.map +1 -1
  31. package/dist/collab/client.d.ts.map +1 -1
  32. package/dist/collab/client.js +26 -2
  33. package/dist/collab/client.js.map +1 -1
  34. package/dist/collab/routes.d.ts +1 -1
  35. package/dist/collab/struct-routes.d.ts +1 -1
  36. package/dist/db/ddl-guard.d.ts +7 -2
  37. package/dist/db/ddl-guard.d.ts.map +1 -1
  38. package/dist/db/ddl-guard.js +21 -9
  39. package/dist/db/ddl-guard.js.map +1 -1
  40. package/dist/deploy/build.d.ts.map +1 -1
  41. package/dist/deploy/build.js +25 -2
  42. package/dist/deploy/build.js.map +1 -1
  43. package/dist/deploy/workspace-deploy.js +14 -4
  44. package/dist/deploy/workspace-deploy.js.map +1 -1
  45. package/dist/notifications/routes.d.ts +1 -1
  46. package/dist/observability/routes.d.ts +1 -1
  47. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  48. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  49. package/dist/realtime-protocol.d.ts +40 -0
  50. package/dist/realtime-protocol.d.ts.map +1 -0
  51. package/dist/realtime-protocol.js +69 -0
  52. package/dist/realtime-protocol.js.map +1 -0
  53. package/dist/resources/handlers.d.ts +1 -1
  54. package/dist/secrets/routes.d.ts +3 -3
  55. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  56. package/dist/server/core-routes-plugin.js +3 -0
  57. package/dist/server/core-routes-plugin.js.map +1 -1
  58. package/dist/server/poll-events.d.ts +7 -1
  59. package/dist/server/poll-events.d.ts.map +1 -1
  60. package/dist/server/poll-events.js +24 -7
  61. package/dist/server/poll-events.js.map +1 -1
  62. package/dist/server/poll.d.ts +223 -42
  63. package/dist/server/poll.d.ts.map +1 -1
  64. package/dist/server/poll.js +849 -737
  65. package/dist/server/poll.js.map +1 -1
  66. package/dist/server/realtime-token.d.ts +34 -0
  67. package/dist/server/realtime-token.d.ts.map +1 -0
  68. package/dist/server/realtime-token.js +85 -0
  69. package/dist/server/realtime-token.js.map +1 -0
  70. package/dist/server/sentry-config.d.ts +12 -0
  71. package/dist/server/sentry-config.d.ts.map +1 -1
  72. package/dist/server/sentry-config.js +34 -0
  73. package/dist/server/sentry-config.js.map +1 -1
  74. package/dist/server/short-lived-token.d.ts +46 -0
  75. package/dist/server/short-lived-token.d.ts.map +1 -1
  76. package/dist/server/short-lived-token.js +104 -0
  77. package/dist/server/short-lived-token.js.map +1 -1
  78. package/dist/server/sse.d.ts +1 -1
  79. package/dist/server/sse.d.ts.map +1 -1
  80. package/dist/server/sse.js +4 -0
  81. package/dist/server/sse.js.map +1 -1
  82. package/dist/server/ssr-handler.d.ts.map +1 -1
  83. package/dist/server/ssr-handler.js +5 -3
  84. package/dist/server/ssr-handler.js.map +1 -1
  85. package/package.json +4 -1
  86. package/src/client/analytics.ts +7 -0
  87. package/src/client/use-db-sync.ts +354 -15
  88. package/src/collab/client.ts +26 -2
  89. package/src/db/ddl-guard.ts +49 -10
  90. package/src/deploy/build.ts +25 -2
  91. package/src/deploy/workspace-deploy.ts +14 -4
  92. package/src/realtime-protocol.ts +92 -0
  93. package/src/server/core-routes-plugin.ts +7 -0
  94. package/src/server/poll-events.ts +29 -8
  95. package/src/server/poll.ts +1123 -865
  96. package/src/server/realtime-token.ts +106 -0
  97. package/src/server/sentry-config.ts +40 -0
  98. package/src/server/short-lived-token.ts +156 -0
  99. package/src/server/sse.ts +15 -0
  100. package/src/server/ssr-handler.ts +9 -3
@@ -1,6 +1,23 @@
1
1
  import { emailToName } from "@agent-native/core/client/collab";
2
2
  import { useActionMutation, useSession } from "@agent-native/core/client/hooks";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
+ import {
5
+ closestCenter,
6
+ DndContext,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ type DragEndEvent,
12
+ } from "@dnd-kit/core";
13
+ import {
14
+ arrayMove,
15
+ SortableContext,
16
+ sortableKeyboardCoordinates,
17
+ useSortable,
18
+ verticalListSortingStrategy,
19
+ } from "@dnd-kit/sortable";
20
+ import { CSS } from "@dnd-kit/utilities";
4
21
  import type {
5
22
  AddContentDatabaseSourceFieldPropertyRequest,
6
23
  BindContentDatabaseSourceFieldRequest,
@@ -30,6 +47,9 @@ import {
30
47
  } from "@shared/properties";
31
48
  import {
32
49
  IconAlignLeft,
50
+ IconArrowLeft,
51
+ IconArrowDown,
52
+ IconArrowUp,
33
53
  IconAt,
34
54
  IconCalendar,
35
55
  IconCheck,
@@ -42,6 +62,8 @@ import {
42
62
  IconEye,
43
63
  IconEyeOff,
44
64
  IconFileText,
65
+ IconFilter,
66
+ IconGripVertical,
45
67
  IconHash,
46
68
  IconLink,
47
69
  IconList,
@@ -49,7 +71,6 @@ import {
49
71
  IconNumber,
50
72
  IconNumber123,
51
73
  IconPaperclip,
52
- IconPalette,
53
74
  IconPhone,
54
75
  IconPlus,
55
76
  IconSearch,
@@ -89,6 +110,7 @@ import {
89
110
  DropdownMenu,
90
111
  DropdownMenuContent,
91
112
  DropdownMenuItem,
113
+ DropdownMenuLabel,
92
114
  DropdownMenuSeparator,
93
115
  DropdownMenuSub,
94
116
  DropdownMenuSubContent,
@@ -118,6 +140,14 @@ import {
118
140
  } from "@/hooks/use-document-properties";
119
141
  import { cn } from "@/lib/utils";
120
142
 
143
+ import {
144
+ clearDatabaseFiltersForColumn,
145
+ clearDatabaseSort,
146
+ databaseQuickFilterOptionsForColumn,
147
+ upsertDatabaseQuickFilter,
148
+ upsertDatabaseSort,
149
+ } from "./database/filter-sort";
150
+ import type { DatabaseFilter, DatabaseSort } from "./database/types";
121
151
  import { imageUploadErrorMessage, uploadImageFile } from "./image-upload";
122
152
 
123
153
  type TFunction = ReturnType<typeof useT>;
@@ -989,6 +1019,12 @@ export function PropertyManagementPopover({
989
1019
  sourceField,
990
1020
  sourceAttached = false,
991
1021
  sources,
1022
+ sorts,
1023
+ filters,
1024
+ onSortsChange,
1025
+ onFiltersChange,
1026
+ onHide,
1027
+ hideDisabled,
992
1028
  }: {
993
1029
  property: DocumentProperty;
994
1030
  documentId: string;
@@ -999,8 +1035,29 @@ export function PropertyManagementPopover({
999
1035
  sourceField?: ContentDatabaseSource["fields"][number] | null;
1000
1036
  sourceAttached?: boolean;
1001
1037
  sources?: ContentDatabaseSource[];
1038
+ sorts?: DatabaseSort[];
1039
+ filters?: DatabaseFilter[];
1040
+ onSortsChange?: (sorts: DatabaseSort[]) => void;
1041
+ onFiltersChange?: (filters: DatabaseFilter[]) => void;
1042
+ onHide?: () => void | Promise<void>;
1043
+ hideDisabled?: boolean;
1002
1044
  }) {
1003
1045
  const t = useT();
1046
+ const hasColumnMenu = !!(
1047
+ sorts &&
1048
+ filters &&
1049
+ onSortsChange &&
1050
+ onFiltersChange
1051
+ );
1052
+ const columnKey = property.definition.id;
1053
+ const columnSort =
1054
+ (sorts ?? []).find((sort) => sort.key === columnKey) ?? null;
1055
+ const columnFilterCount = (filters ?? []).filter(
1056
+ (filter) => filter.key === columnKey,
1057
+ ).length;
1058
+ const quickFilters = databaseQuickFilterOptionsForColumn(
1059
+ property.definition.type,
1060
+ );
1004
1061
  const configure = useConfigureDocumentProperty(documentId);
1005
1062
  const duplicate = useDuplicateDocumentProperty(documentId);
1006
1063
  const remove = useDeleteDocumentProperty(documentId);
@@ -1067,12 +1124,18 @@ export function PropertyManagementPopover({
1067
1124
  blocksFieldCount,
1068
1125
  });
1069
1126
  const [open, setOpen] = useState(false);
1127
+ const [view, setView] = useState<"quick" | "edit">(
1128
+ hasColumnMenu ? "quick" : "edit",
1129
+ );
1070
1130
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
1071
1131
  const [name, setName] = useState(property.definition.name);
1072
1132
  const [description, setDescription] = useState(
1073
1133
  property.definition.description,
1074
1134
  );
1075
1135
  const [newOption, setNewOption] = useState("");
1136
+ const [optionsDraft, setOptionsDraft] = useState<DocumentPropertyOption[]>(
1137
+ property.definition.options.options ?? [],
1138
+ );
1076
1139
  const persistMetadataSnapshotRef = useRef<
1077
1140
  (metadata: PropertyMetadataSnapshot) => Promise<unknown>
1078
1141
  >(async () => undefined);
@@ -1089,6 +1152,7 @@ export function PropertyManagementPopover({
1089
1152
  ),
1090
1153
  );
1091
1154
  const propertyNameInputRef = useRef<HTMLInputElement>(null);
1155
+ const menuContentRef = useRef<HTMLDivElement>(null);
1092
1156
  const typeIsLocked = isComputedPropertyType(property.definition.type);
1093
1157
  const typeNeedsOptions =
1094
1158
  property.definition.type === "select" ||
@@ -1099,6 +1163,7 @@ export function PropertyManagementPopover({
1099
1163
  setName(property.definition.name);
1100
1164
  setDescription(property.definition.description);
1101
1165
  setNewOption("");
1166
+ setOptionsDraft(property.definition.options.options ?? []);
1102
1167
  metadataUpdateQueueRef.current.replace({
1103
1168
  name: property.definition.name,
1104
1169
  type: property.definition.type,
@@ -1112,12 +1177,18 @@ export function PropertyManagementPopover({
1112
1177
  if (!open) return;
1113
1178
 
1114
1179
  const frame = requestAnimationFrame(() => {
1115
- propertyNameInputRef.current?.focus();
1116
- propertyNameInputRef.current?.select();
1180
+ if (view === "edit") {
1181
+ propertyNameInputRef.current?.focus();
1182
+ propertyNameInputRef.current?.select();
1183
+ } else {
1184
+ menuContentRef.current
1185
+ ?.querySelector<HTMLElement>('[role="menuitem"]')
1186
+ ?.focus();
1187
+ }
1117
1188
  });
1118
1189
 
1119
1190
  return () => cancelAnimationFrame(frame);
1120
- }, [open]);
1191
+ }, [open, view]);
1121
1192
 
1122
1193
  async function configureProperty(next: {
1123
1194
  name?: string;
@@ -1138,6 +1209,7 @@ export function PropertyManagementPopover({
1138
1209
  async function updateOptions(
1139
1210
  update: (options: DocumentPropertyOption[]) => DocumentPropertyOption[],
1140
1211
  ) {
1212
+ setOptionsDraft((current) => update(current));
1141
1213
  await metadataUpdateQueueRef.current.enqueue((current) => ({
1142
1214
  ...current,
1143
1215
  options: {
@@ -1153,6 +1225,24 @@ export function PropertyManagementPopover({
1153
1225
  ...metadata,
1154
1226
  });
1155
1227
 
1228
+ const optionDragSensors = useSensors(
1229
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
1230
+ useSensor(KeyboardSensor, {
1231
+ coordinateGetter: sortableKeyboardCoordinates,
1232
+ }),
1233
+ );
1234
+
1235
+ function handleOptionDragEnd(event: DragEndEvent) {
1236
+ const { active, over } = event;
1237
+ if (!over || active.id === over.id) return;
1238
+ void updateOptions((options) => {
1239
+ const fromIndex = options.findIndex((option) => option.id === active.id);
1240
+ const toIndex = options.findIndex((option) => option.id === over.id);
1241
+ if (fromIndex < 0 || toIndex < 0) return options;
1242
+ return arrayMove(options, fromIndex, toIndex);
1243
+ });
1244
+ }
1245
+
1156
1246
  async function renameProperty() {
1157
1247
  const nextName = name.trim();
1158
1248
  if (!nextName || nextName === property.definition.name) return;
@@ -1239,7 +1329,10 @@ export function PropertyManagementPopover({
1239
1329
  <DropdownMenu
1240
1330
  open={open}
1241
1331
  onOpenChange={(nextOpen) => {
1242
- if (nextOpen) resetDraft();
1332
+ if (nextOpen) {
1333
+ resetDraft();
1334
+ setView(hasColumnMenu ? "quick" : "edit");
1335
+ }
1243
1336
  setOpen(nextOpen);
1244
1337
  }}
1245
1338
  >
@@ -1260,6 +1353,7 @@ export function PropertyManagementPopover({
1260
1353
  ? (event) => {
1261
1354
  event.preventDefault();
1262
1355
  resetDraft();
1356
+ setView(hasColumnMenu ? "quick" : "edit");
1263
1357
  setOpen(true);
1264
1358
  }
1265
1359
  : undefined
@@ -1271,292 +1365,443 @@ export function PropertyManagementPopover({
1271
1365
  </button>
1272
1366
  </DropdownMenuTrigger>
1273
1367
  <DropdownMenuContent
1368
+ ref={menuContentRef}
1274
1369
  align="start"
1275
1370
  collisionPadding={12}
1276
- className="relative z-[300] w-72"
1371
+ className="relative z-[300] w-72 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto"
1277
1372
  >
1278
- <div
1279
- className="flex items-center gap-2 p-1"
1280
- onKeyDown={(event) => event.stopPropagation()}
1281
- >
1282
- <IconEdit className="size-4 shrink-0 text-muted-foreground" />
1283
- <Input
1284
- ref={propertyNameInputRef}
1285
- value={name}
1286
- aria-label={t("editor.properties.propertyName")}
1287
- onChange={(event) => setName(event.target.value)}
1288
- onBlur={() => void renameProperty()}
1289
- onKeyDown={(event) => {
1290
- if (event.key === "Enter") {
1373
+ {view === "quick" && hasColumnMenu ? (
1374
+ <>
1375
+ <DropdownMenuLabel className="truncate text-xs text-muted-foreground">
1376
+ {property.definition.name}
1377
+ </DropdownMenuLabel>
1378
+ <DropdownMenuItem
1379
+ onSelect={(event) => {
1291
1380
  event.preventDefault();
1292
- event.currentTarget.blur();
1293
- }
1294
- }}
1295
- className="h-8"
1296
- />
1297
- </div>
1298
-
1299
- <div
1300
- className="px-2 pb-2 pt-1"
1301
- onKeyDown={(event) => event.stopPropagation()}
1302
- >
1303
- <Textarea
1304
- rows={2}
1305
- value={description}
1306
- aria-label={t("editor.properties.description")}
1307
- placeholder={t("editor.properties.addPropertyDescription")}
1308
- onChange={(event) => setDescription(event.target.value)}
1309
- onBlur={() => void updateDescription()}
1310
- className="block w-full resize-none rounded border border-transparent bg-muted/30 px-2 py-1.5 text-xs leading-5 text-muted-foreground outline-none placeholder:text-muted-foreground/60 focus:border-input focus:bg-background focus:ring-1 focus:ring-ring"
1311
- />
1312
- </div>
1313
-
1314
- <DropdownMenuSub>
1315
- <DropdownMenuSubTrigger>
1316
- <Icon className="mr-2 size-4 text-muted-foreground" />
1317
- <span className="flex-1">{t("editor.properties.type")}</span>
1318
- <span className="mr-2 text-muted-foreground">
1319
- {t(`editor.propertyTypes.${property.definition.type}`)}
1320
- </span>
1321
- </DropdownMenuSubTrigger>
1322
- <DropdownMenuSubContent className="max-h-80 w-56 overflow-auto">
1323
- {CREATABLE_DOCUMENT_PROPERTY_TYPES.map((propertyType) => {
1324
- const TypeIcon = TYPE_ICONS[propertyType];
1325
- const selected = property.definition.type === propertyType;
1326
- const disabled = typeIsLocked && !selected;
1327
- return (
1381
+ onSortsChange?.(
1382
+ upsertDatabaseSort(
1383
+ sorts ?? [],
1384
+ columnKey,
1385
+ property.definition.name,
1386
+ "asc",
1387
+ ),
1388
+ );
1389
+ }}
1390
+ >
1391
+ <IconArrowUp className="mr-2 size-4 text-muted-foreground" />
1392
+ <span className="min-w-0 flex-1">
1393
+ {t("database.sortAscending")}
1394
+ </span>
1395
+ {columnSort?.direction === "asc" ? (
1396
+ <IconCheck className="size-4 text-muted-foreground" />
1397
+ ) : null}
1398
+ </DropdownMenuItem>
1399
+ <DropdownMenuItem
1400
+ onSelect={(event) => {
1401
+ event.preventDefault();
1402
+ onSortsChange?.(
1403
+ upsertDatabaseSort(
1404
+ sorts ?? [],
1405
+ columnKey,
1406
+ property.definition.name,
1407
+ "desc",
1408
+ ),
1409
+ );
1410
+ }}
1411
+ >
1412
+ <IconArrowDown className="mr-2 size-4 text-muted-foreground" />
1413
+ <span className="min-w-0 flex-1">
1414
+ {t("database.sortDescending")}
1415
+ </span>
1416
+ {columnSort?.direction === "desc" ? (
1417
+ <IconCheck className="size-4 text-muted-foreground" />
1418
+ ) : null}
1419
+ </DropdownMenuItem>
1420
+ {columnSort ? (
1421
+ <DropdownMenuItem
1422
+ onSelect={(event) => {
1423
+ event.preventDefault();
1424
+ onSortsChange?.(clearDatabaseSort(sorts ?? [], columnKey));
1425
+ }}
1426
+ >
1427
+ <IconX className="mr-2 size-4 text-muted-foreground" />
1428
+ {t("database.clearSort")}
1429
+ </DropdownMenuItem>
1430
+ ) : null}
1431
+ <DropdownMenuSeparator />
1432
+ {quickFilters.map((quickFilter) => (
1433
+ <DropdownMenuItem
1434
+ key={quickFilter.operator}
1435
+ onSelect={(event) => {
1436
+ event.preventDefault();
1437
+ onFiltersChange?.(
1438
+ upsertDatabaseQuickFilter(
1439
+ filters ?? [],
1440
+ columnKey,
1441
+ property.definition.name,
1442
+ quickFilter.operator,
1443
+ ),
1444
+ );
1445
+ }}
1446
+ >
1447
+ <IconFilter className="mr-2 size-4 text-muted-foreground" />
1448
+ {quickFilter.label}
1449
+ </DropdownMenuItem>
1450
+ ))}
1451
+ {columnFilterCount > 0 ? (
1452
+ <DropdownMenuItem
1453
+ onSelect={(event) => {
1454
+ event.preventDefault();
1455
+ onFiltersChange?.(
1456
+ clearDatabaseFiltersForColumn(filters ?? [], columnKey),
1457
+ );
1458
+ }}
1459
+ >
1460
+ <IconX className="mr-2 size-4 text-muted-foreground" />
1461
+ {t("editor.properties.clearFilters", {
1462
+ count: columnFilterCount,
1463
+ })}
1464
+ </DropdownMenuItem>
1465
+ ) : null}
1466
+ {onHide ? (
1467
+ <>
1468
+ <DropdownMenuSeparator />
1328
1469
  <DropdownMenuItem
1329
- key={propertyType}
1330
- disabled={disabled}
1470
+ disabled={hideDisabled}
1331
1471
  onSelect={(event) => {
1332
1472
  event.preventDefault();
1333
- void updateType(propertyType);
1473
+ void onHide();
1334
1474
  }}
1335
1475
  >
1336
- <TypeIcon className="mr-2 size-4 text-muted-foreground" />
1337
- <span className="flex-1">
1338
- {t(`editor.propertyTypes.${propertyType}`)}
1339
- </span>
1340
- {selected ? (
1341
- <IconCheck className="size-4 text-muted-foreground" />
1342
- ) : null}
1476
+ <IconEyeOff className="mr-2 size-4 text-muted-foreground" />
1477
+ {t("database.hideInView")}
1343
1478
  </DropdownMenuItem>
1344
- );
1345
- })}
1346
- </DropdownMenuSubContent>
1347
- </DropdownMenuSub>
1348
-
1349
- <DropdownMenuSub>
1350
- <DropdownMenuSubTrigger>
1351
- <IconEye className="mr-2 size-4 text-muted-foreground" />
1352
- <span className="flex-1">
1353
- {t("editor.properties.visibility")}
1354
- </span>
1355
- <span className="mr-2 text-muted-foreground">
1356
- {t(
1357
- `editor.propertyVisibility.${property.definition.visibility}`,
1358
- )}
1359
- </span>
1360
- </DropdownMenuSubTrigger>
1361
- <DropdownMenuSubContent className="w-56">
1362
- {DOCUMENT_PROPERTY_VISIBILITIES.map((visibility) => (
1479
+ </>
1480
+ ) : null}
1481
+ <DropdownMenuSeparator />
1482
+ <DropdownMenuItem
1483
+ onSelect={(event) => {
1484
+ event.preventDefault();
1485
+ setView("edit");
1486
+ }}
1487
+ >
1488
+ <IconEdit className="mr-2 size-4 text-muted-foreground" />
1489
+ {t("editor.properties.editField")}
1490
+ </DropdownMenuItem>
1491
+ </>
1492
+ ) : (
1493
+ <>
1494
+ {hasColumnMenu ? (
1363
1495
  <DropdownMenuItem
1364
- key={visibility}
1365
1496
  onSelect={(event) => {
1366
1497
  event.preventDefault();
1367
- void updateVisibility(visibility);
1498
+ setView("quick");
1368
1499
  }}
1500
+ className="gap-1.5 py-1 text-xs text-muted-foreground focus:text-foreground"
1369
1501
  >
1370
- <span className="flex-1">
1371
- {t(`editor.propertyVisibility.${visibility}`)}
1372
- </span>
1373
- {property.definition.visibility === visibility ? (
1374
- <IconCheck className="size-4 text-muted-foreground" />
1375
- ) : null}
1502
+ <IconArrowLeft className="size-3.5" />
1503
+ {t("editor.properties.backToColumnMenu")}
1376
1504
  </DropdownMenuItem>
1377
- ))}
1378
- </DropdownMenuSubContent>
1379
- </DropdownMenuSub>
1380
-
1381
- {typeNeedsOptions ? (
1382
- <div className="grid gap-2 px-1 py-2">
1383
- <div className="px-1 text-xs font-medium text-muted-foreground">
1384
- {t("editor.properties.options")}
1385
- </div>
1386
- <div className="grid gap-1">
1387
- {(property.definition.options.options ?? []).map((option) => (
1388
- <PropertyOptionSettingsRow
1389
- key={option.id}
1390
- option={option}
1391
- disabled={configure.isPending}
1392
- onRename={(name) => void renameOption(option.id, name)}
1393
- onDescriptionChange={(description) =>
1394
- void describeOption(option.id, description)
1395
- }
1396
- onColorChange={(color) =>
1397
- void recolorOption(option.id, color)
1398
- }
1399
- onRemove={() => void removeOption(option.id)}
1400
- />
1401
- ))}
1402
- </div>
1403
- <form
1404
- className="flex gap-2"
1405
- onSubmit={(event) => {
1406
- event.preventDefault();
1407
- void addOption();
1408
- }}
1505
+ ) : null}
1506
+ <div
1507
+ className="flex items-center gap-2 p-1"
1508
+ onKeyDown={(event) => event.stopPropagation()}
1409
1509
  >
1510
+ <IconEdit className="size-4 shrink-0 text-muted-foreground" />
1410
1511
  <Input
1411
- value={newOption}
1412
- placeholder={t("editor.properties.addOption")}
1413
- onChange={(event) => setNewOption(event.target.value)}
1414
- onKeyDown={(event) => event.stopPropagation()}
1512
+ ref={propertyNameInputRef}
1513
+ value={name}
1514
+ aria-label={t("editor.properties.propertyName")}
1515
+ onChange={(event) => setName(event.target.value)}
1516
+ onBlur={() => void renameProperty()}
1517
+ onKeyDown={(event) => {
1518
+ if (event.key === "Enter") {
1519
+ event.preventDefault();
1520
+ event.currentTarget.blur();
1521
+ }
1522
+ }}
1415
1523
  className="h-8"
1416
1524
  />
1417
- <Button
1418
- type="submit"
1419
- size="sm"
1420
- variant="secondary"
1421
- disabled={!newOption.trim() || configure.isPending}
1422
- >
1423
- {t("editor.properties.add")}
1424
- </Button>
1425
- </form>
1426
- </div>
1427
- ) : null}
1525
+ </div>
1428
1526
 
1429
- {showBindingEditor ? (
1430
- <>
1431
- <DropdownMenuSeparator />
1432
- <div className="grid gap-1.5 px-2 py-1.5 text-xs">
1433
- <div className="font-medium text-foreground">
1434
- {t("database.sourcesFeedingThisColumn")}
1435
- </div>
1436
- {boundSourceFields.length > 0 ? (
1437
- <div className="grid gap-1">
1438
- {boundSourceFields.map(({ source: src, field }) => (
1439
- <div
1440
- key={field.id}
1441
- className="flex min-w-0 items-center gap-1.5"
1527
+ <div
1528
+ className="px-2 pb-2 pt-1"
1529
+ onKeyDown={(event) => event.stopPropagation()}
1530
+ >
1531
+ <Textarea
1532
+ rows={1}
1533
+ value={description}
1534
+ aria-label={t("editor.properties.description")}
1535
+ placeholder={t("editor.properties.addPropertyDescription")}
1536
+ onChange={(event) => setDescription(event.target.value)}
1537
+ onBlur={() => void updateDescription()}
1538
+ className="block min-h-0 w-full resize-none rounded border border-transparent bg-muted/30 px-2 py-1.5 text-xs leading-5 text-muted-foreground outline-none placeholder:text-muted-foreground/60 focus:resize-y focus:border-input focus:bg-background focus:ring-1 focus:ring-ring"
1539
+ />
1540
+ </div>
1541
+
1542
+ <DropdownMenuSub>
1543
+ <DropdownMenuSubTrigger>
1544
+ <Icon className="mr-2 size-4 text-muted-foreground" />
1545
+ <span className="flex-1">{t("editor.properties.type")}</span>
1546
+ <span className="mr-2 text-muted-foreground">
1547
+ {t(`editor.propertyTypes.${property.definition.type}`)}
1548
+ </span>
1549
+ </DropdownMenuSubTrigger>
1550
+ <DropdownMenuSubContent className="z-[310] max-h-80 w-56 overflow-auto">
1551
+ {CREATABLE_DOCUMENT_PROPERTY_TYPES.map((propertyType) => {
1552
+ const TypeIcon = TYPE_ICONS[propertyType];
1553
+ const selected = property.definition.type === propertyType;
1554
+ const disabled = typeIsLocked && !selected;
1555
+ return (
1556
+ <DropdownMenuItem
1557
+ key={propertyType}
1558
+ disabled={disabled}
1559
+ onSelect={(event) => {
1560
+ event.preventDefault();
1561
+ void updateType(propertyType);
1562
+ }}
1442
1563
  >
1443
- <IconLink className="size-3.5 shrink-0 text-muted-foreground" />
1444
- <span className="min-w-0 flex-1 truncate text-muted-foreground">
1445
- <span className="text-foreground">
1446
- {src.sourceName}
1447
- </span>{" "}
1448
- · {field.sourceFieldLabel}
1564
+ <TypeIcon className="mr-2 size-4 text-muted-foreground" />
1565
+ <span className="flex-1">
1566
+ {t(`editor.propertyTypes.${propertyType}`)}
1449
1567
  </span>
1450
- <button
1451
- type="button"
1452
- aria-label={`Unbind ${field.sourceFieldLabel} from ${src.sourceName}`}
1453
- disabled={bindSourceField.isPending}
1454
- className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50"
1455
- onClick={() =>
1456
- void bindSourceField.mutateAsync({
1457
- documentId,
1458
- sourceFieldId: field.id,
1459
- propertyId: null,
1460
- })
1461
- }
1462
- >
1463
- <IconX className="size-3.5" />
1464
- </button>
1465
- </div>
1466
- ))}
1568
+ {selected ? (
1569
+ <IconCheck className="size-4 text-muted-foreground" />
1570
+ ) : null}
1571
+ </DropdownMenuItem>
1572
+ );
1573
+ })}
1574
+ </DropdownMenuSubContent>
1575
+ </DropdownMenuSub>
1576
+
1577
+ <DropdownMenuSub>
1578
+ <DropdownMenuSubTrigger>
1579
+ <IconEye className="mr-2 size-4 text-muted-foreground" />
1580
+ <span className="flex-1">
1581
+ {t("editor.properties.visibility")}
1582
+ </span>
1583
+ <span className="mr-2 text-muted-foreground">
1584
+ {t(
1585
+ `editor.propertyVisibility.${property.definition.visibility}`,
1586
+ )}
1587
+ </span>
1588
+ </DropdownMenuSubTrigger>
1589
+ <DropdownMenuSubContent className="z-[310] w-56">
1590
+ {DOCUMENT_PROPERTY_VISIBILITIES.map((visibility) => (
1591
+ <DropdownMenuItem
1592
+ key={visibility}
1593
+ onSelect={(event) => {
1594
+ event.preventDefault();
1595
+ void updateVisibility(visibility);
1596
+ }}
1597
+ >
1598
+ <span className="flex-1">
1599
+ {t(`editor.propertyVisibility.${visibility}`)}
1600
+ </span>
1601
+ {property.definition.visibility === visibility ? (
1602
+ <IconCheck className="size-4 text-muted-foreground" />
1603
+ ) : null}
1604
+ </DropdownMenuItem>
1605
+ ))}
1606
+ </DropdownMenuSubContent>
1607
+ </DropdownMenuSub>
1608
+
1609
+ {typeNeedsOptions ? (
1610
+ <div className="grid gap-2 px-1 py-2">
1611
+ <div className="px-1 text-xs font-medium text-muted-foreground">
1612
+ {t("editor.properties.options")}
1467
1613
  </div>
1468
- ) : (
1469
- <div className="text-muted-foreground">
1470
- {t("database.noSourceFieldsBoundYet")}
1614
+ <div className="grid gap-1">
1615
+ <DndContext
1616
+ sensors={optionDragSensors}
1617
+ collisionDetection={closestCenter}
1618
+ onDragEnd={handleOptionDragEnd}
1619
+ >
1620
+ <SortableContext
1621
+ items={optionsDraft.map((option) => option.id)}
1622
+ strategy={verticalListSortingStrategy}
1623
+ >
1624
+ {optionsDraft.map((option) => (
1625
+ <PropertyOptionSettingsRow
1626
+ key={option.id}
1627
+ option={option}
1628
+ disabled={configure.isPending}
1629
+ onRename={(name) =>
1630
+ void renameOption(option.id, name)
1631
+ }
1632
+ onDescriptionChange={(description) =>
1633
+ void describeOption(option.id, description)
1634
+ }
1635
+ onColorChange={(color) =>
1636
+ void recolorOption(option.id, color)
1637
+ }
1638
+ onRemove={() => void removeOption(option.id)}
1639
+ />
1640
+ ))}
1641
+ </SortableContext>
1642
+ </DndContext>
1471
1643
  </div>
1472
- )}
1473
- {bindableSourceFields.length > 0 ? (
1474
- <DropdownMenuSub>
1475
- <DropdownMenuSubTrigger className="mt-0.5 rounded px-1.5 py-1 text-xs">
1476
- <IconPlus className="mr-1.5 size-3.5 text-muted-foreground" />
1477
- {t("database.bindAFieldFromASource")}
1478
- </DropdownMenuSubTrigger>
1479
- <DropdownMenuSubContent className="max-h-80 w-64 overflow-auto">
1480
- {bindableSourceFields.map(({ source: src, field }) => (
1481
- <DropdownMenuItem
1482
- key={field.id}
1483
- disabled={bindSourceField.isPending}
1484
- onSelect={(event) => {
1485
- event.preventDefault();
1486
- void bindSourceField.mutateAsync({
1487
- documentId,
1488
- sourceFieldId: field.id,
1489
- propertyId: property.definition.id,
1490
- });
1491
- }}
1492
- >
1493
- <IconLink className="mr-2 size-3.5 shrink-0 text-muted-foreground" />
1494
- <span className="min-w-0 flex-1 truncate">
1495
- {field.sourceFieldLabel}
1496
- </span>
1497
- <span className="ml-2 shrink-0 truncate text-[11px] text-muted-foreground">
1498
- {src.sourceName}
1499
- </span>
1500
- </DropdownMenuItem>
1501
- ))}
1502
- </DropdownMenuSubContent>
1503
- </DropdownMenuSub>
1504
- ) : null}
1505
- </div>
1506
- </>
1507
- ) : sourceAttached ? (
1508
- <>
1509
- <DropdownMenuSeparator />
1510
- <div className="grid gap-1 px-2 py-1.5 text-xs">
1511
- <div className="font-medium text-foreground">
1512
- {t("editor.properties.source")}
1644
+ <form
1645
+ className="flex gap-2"
1646
+ onSubmit={(event) => {
1647
+ event.preventDefault();
1648
+ void addOption();
1649
+ }}
1650
+ >
1651
+ <Input
1652
+ value={newOption}
1653
+ placeholder={t("editor.properties.addOption")}
1654
+ onChange={(event) => setNewOption(event.target.value)}
1655
+ onKeyDown={(event) => event.stopPropagation()}
1656
+ className="h-8"
1657
+ />
1658
+ <Button
1659
+ type="submit"
1660
+ size="sm"
1661
+ variant="secondary"
1662
+ disabled={!newOption.trim() || configure.isPending}
1663
+ >
1664
+ {t("editor.properties.add")}
1665
+ </Button>
1666
+ </form>
1513
1667
  </div>
1514
- {sourceField ? (
1515
- <>
1516
- <div className="min-w-0 break-words text-muted-foreground">
1517
- {sourceField.sourceFieldLabel} (
1518
- {sourceField.sourceFieldKey})
1668
+ ) : null}
1669
+
1670
+ {showBindingEditor ? (
1671
+ <>
1672
+ <DropdownMenuSeparator />
1673
+ <div className="grid gap-1.5 px-2 py-1.5 text-xs">
1674
+ <div className="font-medium text-foreground">
1675
+ {t("database.sourcesFeedingThisColumn")}
1519
1676
  </div>
1520
- <div className="text-muted-foreground">
1521
- {sourceField.readOnly
1522
- ? t("editor.properties.readOnly")
1523
- : sourceField.writeOwner === "source"
1524
- ? t("editor.properties.sourceOwned")
1525
- : t("editor.properties.localEditsAllowed")}
1677
+ {boundSourceFields.length > 0 ? (
1678
+ <div className="grid gap-1">
1679
+ {boundSourceFields.map(({ source: src, field }) => (
1680
+ <div
1681
+ key={field.id}
1682
+ className="flex min-w-0 items-center gap-1.5"
1683
+ >
1684
+ <IconLink className="size-3.5 shrink-0 text-muted-foreground" />
1685
+ <span className="min-w-0 flex-1 truncate text-muted-foreground">
1686
+ <span className="text-foreground">
1687
+ {src.sourceName}
1688
+ </span>{" "}
1689
+ · {field.sourceFieldLabel}
1690
+ </span>
1691
+ <button
1692
+ type="button"
1693
+ aria-label={`Unbind ${field.sourceFieldLabel} from ${src.sourceName}`}
1694
+ disabled={bindSourceField.isPending}
1695
+ className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50"
1696
+ onClick={() =>
1697
+ void bindSourceField.mutateAsync({
1698
+ documentId,
1699
+ sourceFieldId: field.id,
1700
+ propertyId: null,
1701
+ })
1702
+ }
1703
+ >
1704
+ <IconX className="size-3.5" />
1705
+ </button>
1706
+ </div>
1707
+ ))}
1708
+ </div>
1709
+ ) : (
1710
+ <div className="text-muted-foreground">
1711
+ {t("database.noSourceFieldsBoundYet")}
1712
+ </div>
1713
+ )}
1714
+ {bindableSourceFields.length > 0 ? (
1715
+ <DropdownMenuSub>
1716
+ <DropdownMenuSubTrigger className="mt-0.5 rounded px-1.5 py-1 text-xs">
1717
+ <IconPlus className="mr-1.5 size-3.5 text-muted-foreground" />
1718
+ {t("database.bindAFieldFromASource")}
1719
+ </DropdownMenuSubTrigger>
1720
+ <DropdownMenuSubContent className="z-[310] max-h-80 w-64 overflow-auto">
1721
+ {bindableSourceFields.map(
1722
+ ({ source: src, field }) => (
1723
+ <DropdownMenuItem
1724
+ key={field.id}
1725
+ disabled={bindSourceField.isPending}
1726
+ onSelect={(event) => {
1727
+ event.preventDefault();
1728
+ void bindSourceField.mutateAsync({
1729
+ documentId,
1730
+ sourceFieldId: field.id,
1731
+ propertyId: property.definition.id,
1732
+ });
1733
+ }}
1734
+ >
1735
+ <IconLink className="mr-2 size-3.5 shrink-0 text-muted-foreground" />
1736
+ <span className="min-w-0 flex-1 truncate">
1737
+ {field.sourceFieldLabel}
1738
+ </span>
1739
+ <span className="ml-2 shrink-0 truncate text-[11px] text-muted-foreground">
1740
+ {src.sourceName}
1741
+ </span>
1742
+ </DropdownMenuItem>
1743
+ ),
1744
+ )}
1745
+ </DropdownMenuSubContent>
1746
+ </DropdownMenuSub>
1747
+ ) : null}
1748
+ </div>
1749
+ </>
1750
+ ) : sourceAttached ? (
1751
+ <>
1752
+ <DropdownMenuSeparator />
1753
+ <div className="grid gap-1 px-2 py-1.5 text-xs">
1754
+ <div className="font-medium text-foreground">
1755
+ {t("editor.properties.source")}
1526
1756
  </div>
1527
- </>
1528
- ) : (
1529
- <div className="text-muted-foreground">
1530
- {t("editor.properties.notMappedToBuilder")}
1757
+ {sourceField ? (
1758
+ <>
1759
+ <div className="min-w-0 break-words text-muted-foreground">
1760
+ {sourceField.sourceFieldLabel} (
1761
+ {sourceField.sourceFieldKey})
1762
+ </div>
1763
+ <div className="text-muted-foreground">
1764
+ {sourceField.readOnly
1765
+ ? t("editor.properties.readOnly")
1766
+ : sourceField.writeOwner === "source"
1767
+ ? t("editor.properties.sourceOwned")
1768
+ : t("editor.properties.localEditsAllowed")}
1769
+ </div>
1770
+ </>
1771
+ ) : (
1772
+ <div className="text-muted-foreground">
1773
+ {t("editor.properties.notMappedToBuilder")}
1774
+ </div>
1775
+ )}
1531
1776
  </div>
1532
- )}
1533
- </div>
1534
- </>
1535
- ) : null}
1777
+ </>
1778
+ ) : null}
1536
1779
 
1537
- <DropdownMenuSeparator />
1538
- <DropdownMenuItem
1539
- disabled={duplicate.isPending}
1540
- onSelect={(event) => {
1541
- event.preventDefault();
1542
- void duplicateProperty();
1543
- }}
1544
- >
1545
- <IconCopy className="mr-2 size-4 text-muted-foreground" />
1546
- {t("editor.properties.duplicateProperty")}
1547
- </DropdownMenuItem>
1548
- <DropdownMenuItem
1549
- disabled={remove.isPending}
1550
- className="text-destructive focus:bg-destructive/10 focus:text-destructive"
1551
- onSelect={(event) => {
1552
- event.preventDefault();
1553
- setOpen(false);
1554
- setConfirmDeleteOpen(true);
1555
- }}
1556
- >
1557
- <IconTrash className="mr-2 size-4" />
1558
- {t("editor.properties.deleteProperty")}
1559
- </DropdownMenuItem>
1780
+ <DropdownMenuSeparator />
1781
+ <DropdownMenuItem
1782
+ disabled={duplicate.isPending}
1783
+ onSelect={(event) => {
1784
+ event.preventDefault();
1785
+ void duplicateProperty();
1786
+ }}
1787
+ >
1788
+ <IconCopy className="mr-2 size-4 text-muted-foreground" />
1789
+ {t("editor.properties.duplicateProperty")}
1790
+ </DropdownMenuItem>
1791
+ <DropdownMenuItem
1792
+ disabled={remove.isPending}
1793
+ className="text-destructive focus:bg-destructive/10 focus:text-destructive"
1794
+ onSelect={(event) => {
1795
+ event.preventDefault();
1796
+ setOpen(false);
1797
+ setConfirmDeleteOpen(true);
1798
+ }}
1799
+ >
1800
+ <IconTrash className="mr-2 size-4" />
1801
+ {t("editor.properties.deleteProperty")}
1802
+ </DropdownMenuItem>
1803
+ </>
1804
+ )}
1560
1805
  </DropdownMenuContent>
1561
1806
  </DropdownMenu>
1562
1807
 
@@ -1616,6 +1861,14 @@ function PropertyOptionSettingsRow({
1616
1861
  const [draftDescription, setDraftDescription] = useState(
1617
1862
  option.description ?? "",
1618
1863
  );
1864
+ const {
1865
+ attributes,
1866
+ listeners,
1867
+ setNodeRef,
1868
+ transform,
1869
+ transition,
1870
+ isDragging,
1871
+ } = useSortable({ id: option.id, disabled });
1619
1872
 
1620
1873
  useEffect(() => {
1621
1874
  setDraftName(option.name);
@@ -1634,45 +1887,39 @@ function PropertyOptionSettingsRow({
1634
1887
  }
1635
1888
 
1636
1889
  return (
1637
- <div className="grid gap-1 rounded px-2 py-1 hover:bg-muted/50">
1890
+ <div
1891
+ ref={setNodeRef}
1892
+ style={{ transform: CSS.Transform.toString(transform), transition }}
1893
+ className={cn(
1894
+ "grid gap-1 rounded px-2 py-1 hover:bg-muted/50",
1895
+ isDragging && "relative z-10 bg-muted/50",
1896
+ )}
1897
+ >
1638
1898
  <div className="flex items-center gap-2">
1639
- <span
1640
- aria-hidden
1641
- className={cn("size-3 shrink-0 rounded-full", optionClass(option))}
1642
- />
1643
- <Input
1644
- value={draftName}
1899
+ <button
1900
+ type="button"
1645
1901
  disabled={disabled}
1646
- aria-label={t("editor.properties.renameOption", {
1902
+ aria-label={t("editor.properties.reorderOption", {
1647
1903
  name: option.name,
1648
1904
  })}
1649
- onChange={(event) => setDraftName(event.target.value)}
1650
- onBlur={submitRename}
1651
- onKeyDown={(event) => {
1652
- event.stopPropagation();
1653
- if (event.key === "Enter") {
1654
- event.preventDefault();
1655
- submitRename();
1656
- event.currentTarget.blur();
1657
- }
1658
- if (event.key === "Escape") {
1659
- setDraftName(option.name);
1660
- event.currentTarget.blur();
1661
- }
1662
- }}
1663
- className="h-7 min-w-0 flex-1 border-0 bg-transparent px-1 text-sm shadow-none focus-visible:bg-background focus-visible:ring-1"
1664
- />
1665
- </div>
1666
- <div className="flex items-center justify-between gap-2 pl-5">
1905
+ className="size-5 shrink-0 cursor-grab touch-none rounded text-muted-foreground/60 hover:text-muted-foreground active:cursor-grabbing disabled:opacity-50"
1906
+ {...attributes}
1907
+ {...listeners}
1908
+ >
1909
+ <IconGripVertical className="size-3.5" />
1910
+ </button>
1667
1911
  <DropdownMenuSub>
1668
1912
  <DropdownMenuSubTrigger
1669
1913
  disabled={disabled}
1670
- className="h-7 rounded px-1.5 text-xs text-muted-foreground"
1914
+ aria-label={t("editor.properties.color")}
1915
+ className="size-5 shrink-0 justify-center rounded-full p-0 [&_svg]:hidden"
1671
1916
  >
1672
- <IconPalette className="mr-1.5 size-3.5" />
1673
- {t("editor.properties.color")}
1917
+ <span
1918
+ aria-hidden
1919
+ className={cn("block size-3 rounded-full", optionClass(option))}
1920
+ />
1674
1921
  </DropdownMenuSubTrigger>
1675
- <DropdownMenuSubContent className="w-44">
1922
+ <DropdownMenuSubContent className="z-[310] w-44">
1676
1923
  {OPTION_COLORS.map((color) => (
1677
1924
  <DropdownMenuItem
1678
1925
  key={color}
@@ -1682,32 +1929,52 @@ function PropertyOptionSettingsRow({
1682
1929
  }}
1683
1930
  >
1684
1931
  <span
1685
- aria-hidden
1686
1932
  className={cn(
1687
- "mr-2 size-3 rounded-full",
1933
+ "inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium capitalize",
1688
1934
  OPTION_COLOR_CLASSES[color],
1689
1935
  )}
1690
- />
1691
- <span className="flex-1 capitalize">
1936
+ >
1692
1937
  {t(`editor.propertyOptionColors.${color}`)}
1693
1938
  </span>
1694
1939
  {option.color === color ? (
1695
- <IconCheck className="size-4 text-muted-foreground" />
1940
+ <IconCheck className="ml-auto size-4 shrink-0 text-muted-foreground" />
1696
1941
  ) : null}
1697
1942
  </DropdownMenuItem>
1698
1943
  ))}
1699
1944
  </DropdownMenuSubContent>
1700
1945
  </DropdownMenuSub>
1946
+ <Input
1947
+ value={draftName}
1948
+ disabled={disabled}
1949
+ aria-label={t("editor.properties.renameOption", {
1950
+ name: option.name,
1951
+ })}
1952
+ onChange={(event) => setDraftName(event.target.value)}
1953
+ onBlur={submitRename}
1954
+ onKeyDown={(event) => {
1955
+ event.stopPropagation();
1956
+ if (event.key === "Enter") {
1957
+ event.preventDefault();
1958
+ submitRename();
1959
+ event.currentTarget.blur();
1960
+ }
1961
+ if (event.key === "Escape") {
1962
+ setDraftName(option.name);
1963
+ event.currentTarget.blur();
1964
+ }
1965
+ }}
1966
+ className="h-7 min-w-0 flex-1 border-0 bg-transparent px-1 text-sm shadow-none focus-visible:bg-background focus-visible:ring-1"
1967
+ />
1701
1968
  <button
1702
1969
  type="button"
1703
1970
  aria-label={t("editor.properties.removeOption", {
1704
1971
  name: option.name,
1705
1972
  })}
1706
1973
  disabled={disabled}
1707
- className="h-7 rounded px-1.5 text-xs text-muted-foreground hover:text-destructive disabled:opacity-50"
1974
+ className="h-7 shrink-0 rounded px-1.5 text-muted-foreground hover:text-destructive disabled:opacity-50"
1708
1975
  onClick={onRemove}
1709
1976
  >
1710
- {t("editor.properties.remove")}
1977
+ <IconX className="size-3.5" />
1711
1978
  </button>
1712
1979
  </div>
1713
1980
  <Textarea
@@ -1723,7 +1990,7 @@ function PropertyOptionSettingsRow({
1723
1990
  onDescriptionChange(nextDescription);
1724
1991
  }
1725
1992
  }}
1726
- className="ml-5 block w-[calc(100%-1.25rem)] resize-none rounded border-0 bg-transparent px-1 text-xs leading-5 text-muted-foreground shadow-none placeholder:text-muted-foreground/60 focus:bg-background focus:ring-1 focus:ring-ring"
1993
+ className="block min-h-0 w-full resize-none rounded border-0 bg-transparent px-1 text-xs leading-5 text-muted-foreground shadow-none placeholder:text-muted-foreground/60 focus:resize-y focus:bg-background focus:ring-1 focus:ring-ring"
1727
1994
  />
1728
1995
  </div>
1729
1996
  );