betterstart-cli 0.0.95 → 0.0.97

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 (162) hide show
  1. package/dist/assets/adapters/next/integrations/_shared/actions/object-key.ts +16 -0
  2. package/dist/assets/adapters/next/integrations/r2/actions/r2.ts +6 -14
  3. package/dist/assets/adapters/next/integrations/r2/integration.ts +4 -0
  4. package/dist/assets/adapters/next/integrations/railway-bucket/actions/railway-bucket.ts +7 -15
  5. package/dist/assets/adapters/next/integrations/railway-bucket/integration.ts +4 -0
  6. package/dist/assets/adapters/next/integrations/vercel-blob/actions/vercel-blob.ts +2 -10
  7. package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +4 -0
  8. package/dist/assets/adapters/next/templates/init/api/upload-route.ts +37 -13
  9. package/dist/assets/adapters/next/templates/init/components/layouts/admin-header.tsx +6 -6
  10. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-branding-skeleton.tsx +2 -2
  11. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu-skeleton.tsx +1 -1
  12. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx +8 -8
  13. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +4 -4
  14. package/dist/assets/adapters/next/templates/init/components/layouts/content-skeleton.tsx +4 -4
  15. package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +4 -4
  16. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +1 -1
  17. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table-pagination.tsx +3 -3
  18. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +1 -1
  19. package/dist/assets/adapters/next/templates/init/components/shared/delete-dialog.tsx +1 -1
  20. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/copyable-code-block.tsx +5 -5
  21. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/plain-code-fallback.tsx +1 -1
  22. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  23. package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +5 -5
  24. package/dist/assets/adapters/next/templates/init/components/shared/entity-metadata.tsx +2 -2
  25. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-version-item.tsx +1 -1
  26. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-current-row.tsx +1 -1
  27. package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +3 -3
  28. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +1 -1
  29. package/dist/assets/adapters/next/templates/init/components/shared/media/media-filters-bar.tsx +2 -2
  30. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +2 -2
  31. package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx +3 -3
  32. package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-pagination.tsx +4 -4
  33. package/dist/assets/adapters/next/templates/init/components/shared/media/media-preview.tsx +3 -3
  34. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +3 -3
  35. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +4 -4
  36. package/dist/assets/adapters/next/templates/init/components/shared/search-input.tsx +5 -5
  37. package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +1 -1
  38. package/dist/assets/adapters/next/templates/init/components/shared/sort-order-dialog.tsx +1 -1
  39. package/dist/assets/adapters/next/templates/init/components/shared/sort-order-drag-overlay-item.tsx +1 -1
  40. package/dist/assets/adapters/next/templates/init/components/shared/sort-order-item.tsx +2 -2
  41. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-color-highlight.ts +10 -64
  42. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-source-mode.tsx +11 -4
  43. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-heading-dropdown-menu.ts +0 -1
  44. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-list-dropdown-menu.ts +0 -1
  45. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +5 -10
  46. package/dist/assets/adapters/next/templates/init/lib/actions/forms/index.ts +1 -5
  47. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +6 -6
  48. package/dist/assets/adapters/next/templates/init/lib/actions/media/get-media.ts +4 -1
  49. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +5 -5
  50. package/dist/assets/adapters/next/templates/init/lib/actions/profile/index.ts +1 -5
  51. package/dist/assets/adapters/next/templates/init/lib/actions/upload/index.ts +1 -5
  52. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +1 -10
  53. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +1 -10
  54. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +4 -1
  55. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +1 -1
  56. package/dist/assets/adapters/next/templates/init/pages/admin-layout.tsx +1 -1
  57. package/dist/assets/adapters/next/templates/init/pages/auth-gate-rsc.tsx +1 -1
  58. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +6 -6
  59. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +4 -4
  60. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +3 -3
  61. package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +3 -3
  62. package/dist/assets/adapters/next/templates/init/pages/media/media-page-skeleton.tsx +1 -1
  63. package/dist/assets/adapters/next/templates/init/pages/minimal-account-shell.tsx +2 -2
  64. package/dist/assets/adapters/next/templates/init/pages/profile/profile-page.tsx +1 -1
  65. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +3 -3
  66. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx +1 -1
  67. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-drawer.tsx +1 -1
  68. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-row.tsx +2 -2
  69. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx +1 -1
  70. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-skeleton.tsx +1 -1
  71. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +2 -2
  72. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +1 -1
  73. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-skeleton.tsx +1 -1
  74. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +1 -1
  75. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +3 -3
  76. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-columns.tsx +6 -6
  77. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx +7 -7
  78. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +1 -1
  79. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +2 -2
  80. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-content.tsx +1 -1
  81. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-skeleton.tsx +1 -1
  82. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +2 -2
  83. package/dist/assets/adapters/next/templates/init/pages/users/columns.tsx +4 -4
  84. package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +1 -1
  85. package/dist/assets/adapters/next/templates/init/pages/users/users-page-skeleton.tsx +1 -1
  86. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +5 -5
  87. package/dist/assets/adapters/next/templates/init/types/table-meta.ts +1 -1
  88. package/dist/assets/adapters/next/templates/init/utils/date/date.ts +15 -0
  89. package/dist/assets/adapters/next/templates/init/utils/editor/content-editor.ts +98 -25
  90. package/dist/assets/adapters/next/templates/init/utils/editor/markdown.ts +5 -2
  91. package/dist/assets/shared-assets/react-admin/custom/content-editor/editor-toolbar.tsx +1 -1
  92. package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +2 -2
  93. package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +4 -4
  94. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
  95. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +1 -1
  96. package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +1 -1
  97. package/dist/assets/shared-assets/react-admin/custom/content-editor/remove-table-part-icon.tsx +1 -1
  98. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +5 -5
  99. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +6 -6
  100. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +11 -9
  101. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-add-controls.tsx +3 -3
  102. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +2 -2
  103. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/removable-image-node/removable-image-node.tsx +1 -1
  104. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -2
  105. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -2
  106. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-button/color-highlight-button.tsx +2 -3
  107. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +4 -2
  108. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +3 -2
  109. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +5 -3
  110. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +2 -1
  111. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +2 -2
  112. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -2
  113. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -2
  114. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -2
  115. package/dist/assets/shared-assets/react-admin/custom/date-picker.tsx +2 -2
  116. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +6 -6
  117. package/dist/assets/shared-assets/react-admin/custom/icon-picker.tsx +10 -8
  118. package/dist/assets/shared-assets/react-admin/custom/icons-column-skeleton.tsx +2 -2
  119. package/dist/assets/shared-assets/react-admin/custom/media-field-empty-state.tsx +5 -5
  120. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +7 -7
  121. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +2 -2
  122. package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
  123. package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
  124. package/dist/assets/shared-assets/react-admin/custom/progressive-blur.tsx +1 -1
  125. package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +3 -3
  126. package/dist/assets/shared-assets/react-admin/custom/upload-dropzone.tsx +6 -6
  127. package/dist/assets/shared-assets/react-admin/schema.json +1143 -876
  128. package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
  129. package/dist/assets/shared-assets/react-admin/ui/alert-dialog.tsx +4 -4
  130. package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -1
  131. package/dist/assets/shared-assets/react-admin/ui/button-group.tsx +1 -1
  132. package/dist/assets/shared-assets/react-admin/ui/button.tsx +6 -6
  133. package/dist/assets/shared-assets/react-admin/ui/card.tsx +5 -5
  134. package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
  135. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +1 -1
  136. package/dist/assets/shared-assets/react-admin/ui/command.tsx +0 -1
  137. package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +2 -2
  138. package/dist/assets/shared-assets/react-admin/ui/dialog.tsx +3 -3
  139. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +1 -1
  140. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +3 -3
  141. package/dist/assets/shared-assets/react-admin/ui/field.tsx +0 -1
  142. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +2 -2
  143. package/dist/assets/shared-assets/react-admin/ui/input.tsx +1 -1
  144. package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +2 -2
  145. package/dist/assets/shared-assets/react-admin/ui/pagination.tsx +0 -1
  146. package/dist/assets/shared-assets/react-admin/ui/scroll-area.tsx +1 -1
  147. package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
  148. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +4 -4
  149. package/dist/assets/shared-assets/react-admin/ui/table.tsx +1 -1
  150. package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
  151. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  152. package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
  153. package/dist/{chunk-3CLINNJL.js → chunk-MJWUGEMK.js} +32 -8
  154. package/dist/chunk-MJWUGEMK.js.map +1 -0
  155. package/dist/cli.js +8554 -9062
  156. package/dist/cli.js.map +1 -1
  157. package/dist/index.d.ts +1 -1
  158. package/dist/index.js.map +1 -1
  159. package/dist/{template-reader-PVN53GS7.js → template-reader-R2JIWUNK.js} +2 -2
  160. package/package.json +3 -3
  161. package/dist/chunk-3CLINNJL.js.map +0 -1
  162. /package/dist/{template-reader-PVN53GS7.js.map → template-reader-R2JIWUNK.js.map} +0 -0
@@ -0,0 +1,16 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ function sanitizeFilename(filename: string): string {
4
+ return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
5
+ }
6
+
7
+ /**
8
+ * Object-key naming scheme for every upload, shared by all storage providers.
9
+ *
10
+ * Single owner: a project that switches storage providers must keep producing
11
+ * consistent keys. This previously existed as an identical copy inside each of
12
+ * the R2, Railway Bucket, and Vercel Blob payloads.
13
+ */
14
+ export function buildStorageObjectKey(filename: string, prefix = 'uploads'): string {
15
+ return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
16
+ }
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'node:crypto'
1
+ import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
2
2
  import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
3
3
  import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
4
4
 
@@ -14,14 +14,6 @@ function getConfig() {
14
14
  }
15
15
  }
16
16
 
17
- function sanitizeFilename(filename: string): string {
18
- return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
19
- }
20
-
21
- function buildKey(filename: string, prefix = 'uploads'): string {
22
- return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
23
- }
24
-
25
17
  function getClient(): S3Client {
26
18
  if (r2Client) {
27
19
  return r2Client
@@ -46,10 +38,10 @@ export const r2StorageProvider: AdminStorageProvider = {
46
38
  const config = getConfig()
47
39
  return Boolean(
48
40
  config.accountId &&
49
- config.accessKeyId &&
50
- config.secretAccessKey &&
51
- config.bucketName &&
52
- config.publicUrl
41
+ config.accessKeyId &&
42
+ config.secretAccessKey &&
43
+ config.bucketName &&
44
+ config.publicUrl
53
45
  )
54
46
  },
55
47
  async saveFile(input: AdminStorageUploadInput) {
@@ -58,7 +50,7 @@ export const r2StorageProvider: AdminStorageProvider = {
58
50
  throw new Error('Cloudflare R2 is not fully configured.')
59
51
  }
60
52
 
61
- const key = buildKey(input.filename, input.prefix)
53
+ const key = buildStorageObjectKey(input.filename, input.prefix)
62
54
  await getClient().send(
63
55
  new PutObjectCommand({
64
56
  Bucket: config.bucketName,
@@ -9,6 +9,10 @@ export const r2Integration: BetterstartIntegrationDefinition = {
9
9
  dependencies: [],
10
10
  conflicts: ['vercel-blob', 'railway-bucket'],
11
11
  files: [
12
+ {
13
+ outputPath: 'lib/actions/storage/object-key.ts',
14
+ templatePath: '../_shared/actions/object-key.ts'
15
+ },
12
16
  {
13
17
  outputPath: 'lib/actions/r2/provider.ts',
14
18
  templatePath: 'actions/r2.ts'
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'node:crypto'
1
+ import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
2
2
  import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
3
3
  import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
4
4
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
@@ -16,14 +16,6 @@ function getConfig() {
16
16
  }
17
17
  }
18
18
 
19
- function sanitizeFilename(filename: string): string {
20
- return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
21
- }
22
-
23
- function buildKey(filename: string, prefix = 'uploads'): string {
24
- return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
25
- }
26
-
27
19
  function stableReadUrl(key: string): string {
28
20
  return `/api/admin/storage/${Buffer.from(key, 'utf-8').toString('base64url')}`
29
21
  }
@@ -50,11 +42,11 @@ export const railwayBucketStorageProvider = {
50
42
  const config = getConfig()
51
43
  return Boolean(
52
44
  config.endpoint &&
53
- config.accessKeyId &&
54
- config.secretAccessKey &&
55
- config.bucketName &&
56
- config.region &&
57
- config.urlStyle
45
+ config.accessKeyId &&
46
+ config.secretAccessKey &&
47
+ config.bucketName &&
48
+ config.region &&
49
+ config.urlStyle
58
50
  )
59
51
  },
60
52
  async saveFile(input: AdminStorageUploadInput) {
@@ -63,7 +55,7 @@ export const railwayBucketStorageProvider = {
63
55
  throw new Error('Railway Bucket is not fully configured.')
64
56
  }
65
57
 
66
- const key = buildKey(input.filename, input.prefix)
58
+ const key = buildStorageObjectKey(input.filename, input.prefix)
67
59
  await getClient().send(
68
60
  new PutObjectCommand({
69
61
  Bucket: config.bucketName,
@@ -9,6 +9,10 @@ export const railwayBucketIntegration: BetterstartIntegrationDefinition = {
9
9
  dependencies: [],
10
10
  conflicts: ['r2', 'vercel-blob'],
11
11
  files: [
12
+ {
13
+ outputPath: 'lib/actions/storage/object-key.ts',
14
+ templatePath: '../_shared/actions/object-key.ts'
15
+ },
12
16
  {
13
17
  outputPath: 'lib/actions/railway-bucket/provider.ts',
14
18
  templatePath: 'actions/railway-bucket.ts'
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'node:crypto'
1
+ import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
2
2
  import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
3
3
  import { put } from '@vercel/blob'
4
4
 
@@ -6,14 +6,6 @@ function getToken(): string {
6
6
  return process.env.BLOB_READ_WRITE_TOKEN?.trim() ?? ''
7
7
  }
8
8
 
9
- function sanitizeFilename(filename: string): string {
10
- return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
11
- }
12
-
13
- function buildKey(filename: string, prefix = 'uploads'): string {
14
- return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
15
- }
16
-
17
9
  export const vercelBlobStorageProvider: AdminStorageProvider = {
18
10
  id: 'vercel-blob',
19
11
  isConfigured() {
@@ -24,7 +16,7 @@ export const vercelBlobStorageProvider: AdminStorageProvider = {
24
16
  throw new Error('Vercel Blob is not configured.')
25
17
  }
26
18
 
27
- const key = buildKey(input.filename, input.prefix)
19
+ const key = buildStorageObjectKey(input.filename, input.prefix)
28
20
  const blob = await put(key, input.buffer, {
29
21
  access: 'public',
30
22
  contentType: input.contentType,
@@ -9,6 +9,10 @@ export const vercelBlobIntegration: BetterstartIntegrationDefinition = {
9
9
  dependencies: [],
10
10
  conflicts: ['r2', 'railway-bucket'],
11
11
  files: [
12
+ {
13
+ outputPath: 'lib/actions/storage/object-key.ts',
14
+ templatePath: '../_shared/actions/object-key.ts'
15
+ },
12
16
  {
13
17
  outputPath: 'lib/actions/vercel-blob/provider.ts',
14
18
  templatePath: 'actions/vercel-blob.ts'
@@ -1,9 +1,12 @@
1
+ import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
1
2
  import { mediaCacheTags } from '@admin/actions/media/types'
2
3
  import { saveUpload } from '@admin/actions/storage'
3
- import { getSession } from '@admin/auth/middleware'
4
+ import { requireRole, UserRole } from '@admin/auth/middleware'
4
5
  import db from '@admin/db'
5
6
  import { adminMedia } from '@admin/db/schema'
7
+ import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
6
8
  import type { UploadedFile } from '@admin/types'
9
+ import { getTouchedFieldNames } from '@admin/utils/audit/audit'
7
10
  import { mapMediaRow, selectMedia } from '@admin/utils/media/query'
8
11
  import { validateFiles } from '@admin/utils/validation/validation'
9
12
  import { eq } from 'drizzle-orm'
@@ -11,6 +14,8 @@ import { revalidateTag } from 'next/cache'
11
14
  import { type NextRequest, NextResponse } from 'next/server'
12
15
 
13
16
  export async function POST(request: NextRequest) {
17
+ const actor = await requireRole([UserRole.ADMIN, UserRole.EDITOR])
18
+
14
19
  try {
15
20
  const formData = await request.formData()
16
21
  const prefix = formData.get('prefix')?.toString() || 'uploads'
@@ -56,8 +61,7 @@ export async function POST(request: NextRequest) {
56
61
  }
57
62
 
58
63
  const uploadedFiles: UploadedFile[] = []
59
- const session = await getSession()
60
- const actorId = session?.user?.id ?? null
64
+ const auditRequestMetadata = await getAuditRequestMetadata()
61
65
 
62
66
  for (const [index, file] of files.entries()) {
63
67
  const arrayBuffer = await file.arrayBuffer()
@@ -74,21 +78,25 @@ export async function POST(request: NextRequest) {
74
78
  const heightStr = formData.get(`height${index}`)?.toString()
75
79
  const now = new Date().toISOString()
76
80
 
81
+ const mediaValues = {
82
+ url,
83
+ filename: file.name,
84
+ contentType: file.type,
85
+ size: file.size,
86
+ width: widthStr ? Number(widthStr) : null,
87
+ height: heightStr ? Number(heightStr) : null,
88
+ alt: null,
89
+ tags: null
90
+ }
91
+
77
92
  const [created] = await db
78
93
  .insert(adminMedia)
79
94
  .values({
80
- url,
81
- filename: file.name,
82
- contentType: file.type,
83
- size: file.size,
84
- width: widthStr ? Number(widthStr) : null,
85
- height: heightStr ? Number(heightStr) : null,
86
- alt: null,
87
- tags: null,
95
+ ...mediaValues,
88
96
  createdAt: now,
89
97
  updatedAt: now,
90
- createdBy: actorId,
91
- updatedBy: actorId
98
+ createdBy: actor.id,
99
+ updatedBy: actor.id
92
100
  })
93
101
  .returning({ id: adminMedia.id })
94
102
 
@@ -99,6 +107,8 @@ export async function POST(request: NextRequest) {
99
107
  )
100
108
  }
101
109
 
110
+ // Route handlers cannot call updateTag(), so revalidateTag() with an
111
+ // immediate expiry is the equivalent invalidation used by the actions.
102
112
  revalidateTag(mediaCacheTags.all, { expire: 0 })
103
113
  revalidateTag(mediaCacheTags.byId(created.id), { expire: 0 })
104
114
 
@@ -112,6 +122,20 @@ export async function POST(request: NextRequest) {
112
122
 
113
123
  const media = mapMediaRow(mediaRow)
114
124
 
125
+ runAdminEventHooks({
126
+ category: 'media',
127
+ action: 'create',
128
+ targetType: 'media',
129
+ targetId: media.id,
130
+ targetLabel: media.filename,
131
+ user: actor,
132
+ timestamp: now,
133
+ sourceType: 'admin_ui' as const,
134
+ changedFields: getTouchedFieldNames(mediaValues),
135
+ metadata: { contentType: media.contentType, size: media.size },
136
+ ...auditRequestMetadata
137
+ })
138
+
115
139
  uploadedFiles.push({
116
140
  id: media.id,
117
141
  key,
@@ -10,19 +10,19 @@ export function AdminHeader() {
10
10
  const { state } = useSidebar()
11
11
 
12
12
  return (
13
- <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
14
- <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
15
- <div className="flex items-center gap-2 w-full">
13
+ <header className="sticky top-0 z-50 flex h-14 w-full shrink-0 items-center gap-2 border-b border-border">
14
+ <div className="flex w-full flex-1 items-center justify-between gap-1 px-5">
15
+ <div className="flex w-full items-center gap-2">
16
16
  {state === 'collapsed' && <SidebarTrigger />}
17
17
  </div>
18
- <div className="flex items-center gap-2 ml-auto">
18
+ <div className="ml-auto flex items-center gap-2">
19
19
  <Button
20
20
  variant="ghost"
21
21
  size="icon"
22
22
  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
23
23
  >
24
- <Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
25
- <Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
24
+ <Sun className="size-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
25
+ <Moon className="absolute size-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
26
26
  <span className="sr-only">Toggle theme</span>
27
27
  </Button>
28
28
  </div>
@@ -2,8 +2,8 @@ import { Skeleton } from '@admin/components/ui/skeleton'
2
2
 
3
3
  export function BrandingSkeleton() {
4
4
  return (
5
- <div className="flex items-center gap-2 w-full h-full">
6
- <Skeleton className="size-8 rounded-sm shrink-0" />
5
+ <div className="flex h-full w-full items-center gap-2">
6
+ <Skeleton className="size-8 shrink-0 rounded-sm" />
7
7
  <Skeleton className="h-4 w-24 group-data-[collapsible=icon]:hidden" />
8
8
  </div>
9
9
  )
@@ -6,7 +6,7 @@ export function UserMenuSkeleton() {
6
6
  <SidebarMenu className="h-full gap-0">
7
7
  <SidebarMenuItem className="h-full">
8
8
  <div className="flex h-full w-full items-center gap-2 px-3">
9
- <Skeleton className="size-8 rounded-sm shrink-0" />
9
+ <Skeleton className="size-8 shrink-0 rounded-sm" />
10
10
  <Skeleton className="h-4 w-20 group-data-[collapsible=icon]:hidden" />
11
11
  </div>
12
12
  </SidebarMenuItem>
@@ -27,7 +27,7 @@ export async function SidebarUserMenu() {
27
27
  <DropdownMenuTrigger asChild>
28
28
  <Button
29
29
  variant="ghost"
30
- className="bg-transparent! px-3 h-full w-full justify-start text-left ring-0! border-none"
30
+ className="h-full w-full justify-start border-none bg-transparent! px-3 text-left ring-0!"
31
31
  >
32
32
  <Avatar className="size-8 shrink-0">
33
33
  <AvatarImage src={user.image ?? undefined} />
@@ -36,14 +36,14 @@ export async function SidebarUserMenu() {
36
36
  </AvatarFallback>
37
37
  </Avatar>
38
38
  <div className="flex-1 truncate group-data-[collapsible=icon]:hidden">
39
- <p className="font-medium truncate">{user.name}</p>
39
+ <p className="truncate font-medium">{user.name}</p>
40
40
  </div>
41
41
 
42
42
  <div className="group-data-[collapsible=icon]:hidden">
43
43
  <span
44
44
  className={cn(
45
45
  buttonVariants({ variant: 'outline', size: 'icon' }),
46
- 'rounded-sm group-hover:bg-white size-7'
46
+ 'size-7 rounded-sm group-hover:bg-white'
47
47
  )}
48
48
  >
49
49
  <Ellipsis strokeWidth={2} className="size-4" />
@@ -57,17 +57,17 @@ export async function SidebarUserMenu() {
57
57
  <Link
58
58
  href="/admin/profile"
59
59
  prefetch={true}
60
- className="w-full flex justify-between items-center gap-2 pr-1 py-2"
60
+ className="flex w-full items-center justify-between gap-2 py-2 pr-1"
61
61
  >
62
- <div className="flex gap-2 items-center">
62
+ <div className="flex items-center gap-2">
63
63
  <div className="flex flex-col">
64
- <p className="font-medium truncate">{user.name}</p>
65
- <p className="text-sm text-muted-foreground [text-box-trim:trim-start] truncate font-normal">
64
+ <p className="truncate font-medium">{user.name}</p>
65
+ <p className="truncate text-sm font-normal text-muted-foreground [text-box-trim:trim-start]">
66
66
  {user.email}
67
67
  </p>
68
68
  </div>
69
69
  </div>
70
- <Button variant="ghost" className="size-8! pointer-events-none shrink-0">
70
+ <Button variant="ghost" className="pointer-events-none size-8! shrink-0">
71
71
  <Settings strokeWidth={2} className="size-4" />
72
72
  </Button>
73
73
  </Link>
@@ -25,12 +25,12 @@ export function AdminSidebar(props: ComponentProps<typeof Sidebar>) {
25
25
 
26
26
  return (
27
27
  <Sidebar collapsible="icon" {...props}>
28
- <SidebarHeader className="border-b border-border/80 h-14 items-center flex w-full border-t">
28
+ <SidebarHeader className="flex h-14 w-full items-center border-t border-b border-border/80">
29
29
  <Suspense fallback={<BrandingSkeleton />}>
30
30
  <SidebarBrandingRsc />
31
31
  </Suspense>
32
32
  </SidebarHeader>
33
- <SidebarContent className="pb-1 pt-4">
33
+ <SidebarContent className="pt-4 pb-1">
34
34
  {groups.map((group) => (
35
35
  <Fragment key={group.label ?? '_ungrouped'}>
36
36
  <SidebarGroup className="py-0">
@@ -51,7 +51,7 @@ export function AdminSidebar(props: ComponentProps<typeof Sidebar>) {
51
51
 
52
52
  <SidebarGroup className="mt-auto gap-2 px-0">
53
53
  <Separator />
54
- <SidebarMenu className="px-2 gap-0.5">
54
+ <SidebarMenu className="gap-0.5 px-2">
55
55
  <SidebarMenuItem>
56
56
  <SidebarNavLink href="/admin/users">
57
57
  <Users strokeWidth={2} />
@@ -68,7 +68,7 @@ export function AdminSidebar(props: ComponentProps<typeof Sidebar>) {
68
68
  </SidebarGroup>
69
69
  </SidebarContent>
70
70
 
71
- <SidebarFooter className="[--admin-shell-bottom-bar-height:53px] h-(--admin-shell-bottom-bar-height) shrink-0 border-t border-border bg-background p-0">
71
+ <SidebarFooter className="h-(--admin-shell-bottom-bar-height) shrink-0 border-t border-border bg-background p-0 [--admin-shell-bottom-bar-height:53px]">
72
72
  <Suspense fallback={<UserMenuSkeleton />}>
73
73
  <SidebarUserMenu />
74
74
  </Suspense>
@@ -3,9 +3,9 @@ import { Skeleton } from '@admin/components/ui/skeleton'
3
3
 
4
4
  export function ContentSkeleton() {
5
5
  return (
6
- <div className="relative z-10 flex items-center justify-center h-[50svh] px-4">
6
+ <div className="relative z-10 flex h-[50svh] items-center justify-center px-4">
7
7
  <div className="relative w-full max-w-xl" aria-hidden="true">
8
- <Card size="sm" className="absolute left-1/2 top-1 h-24 w-[72%] -translate-x-1/2 rotate-3">
8
+ <Card size="sm" className="absolute top-1 left-1/2 h-24 w-[72%] -translate-x-1/2 rotate-3">
9
9
  <CardContent className="flex h-full items-start gap-3 p-3">
10
10
  <Skeleton className="size-8 shrink-0 rounded-md" />
11
11
  <div className="flex min-w-0 flex-1 flex-col gap-2 pt-1">
@@ -16,7 +16,7 @@ export function ContentSkeleton() {
16
16
  </CardContent>
17
17
  </Card>
18
18
 
19
- <Card size="sm" className="absolute right-2 top-10 h-36 w-[50%] rotate-6">
19
+ <Card size="sm" className="absolute top-10 right-2 h-36 w-[50%] rotate-6">
20
20
  <CardContent className="flex h-full flex-col gap-4 p-4">
21
21
  <Skeleton className="ml-auto h-6 w-14 rounded-md" />
22
22
  <div className="flex flex-col gap-3 pt-2">
@@ -27,7 +27,7 @@ export function ContentSkeleton() {
27
27
  </CardContent>
28
28
  </Card>
29
29
 
30
- <Card size="sm" className="absolute left-0 top-12 h-36 w-[78%] -rotate-2">
30
+ <Card size="sm" className="absolute top-12 left-0 h-36 w-[78%] -rotate-2">
31
31
  <CardContent className="flex h-full gap-4 p-4">
32
32
  <Skeleton className="h-full w-[27%] shrink-0 rounded-md" />
33
33
  <div className="flex min-w-0 flex-1 flex-col gap-3">
@@ -24,16 +24,16 @@ export function SidebarBranding({ initialData }: SidebarBrandingProps) {
24
24
  const dashboardLogo = settings?.dashboardLogo || DEFAULT_LOGO_URL
25
25
 
26
26
  return (
27
- <div className="flex items-center gap-2 w-full relative h-full">
28
- <Link href="/admin" className="flex items-center gap-2 w-full">
27
+ <div className="relative flex h-full w-full items-center gap-2">
28
+ <Link href="/admin" className="flex w-full items-center gap-2">
29
29
  <Avatar className="size-8">
30
30
  <AvatarImage src={dashboardLogo} />
31
31
  <AvatarFallback className="text-sm font-semibold text-foreground">
32
32
  {dashboardName.charAt(0)}
33
33
  </AvatarFallback>
34
34
  </Avatar>
35
- <div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
36
- <span className="text-sm font-medium line-clamp-1">{dashboardName}</span>
35
+ <div className="flex w-full items-center gap-1 text-foreground group-data-[collapsible=icon]:hidden">
36
+ <span className="line-clamp-1 text-sm font-medium">{dashboardName}</span>
37
37
  </div>
38
38
  </Link>
39
39
  </div>
@@ -90,7 +90,7 @@ function DataGridHead({ className, ...props }: React.ComponentProps<'div'>) {
90
90
  {...dataGridColumnHeaderRole}
91
91
  data-slot="data-grid-head"
92
92
  className={cn(
93
- 'grid h-9.5 items-center px-3 text-left font-medium whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0 *:h-full',
93
+ 'grid h-9.5 items-center px-3 text-left font-medium whitespace-nowrap text-muted-foreground *:h-full [&:has([role=checkbox])]:pr-0',
94
94
  className
95
95
  )}
96
96
  tabIndex={-1}
@@ -90,19 +90,19 @@ export function DataTablePagination<TData>(props: DataTablePaginationProps<TData
90
90
  return (
91
91
  <div
92
92
  className={cn(
93
- 'pointer-events-none fixed right-0 bottom-0 left-[var(--admin-shell-content-left)] z-10 isolate flex flex-col transition-[left] duration-200 ease-linear',
93
+ 'pointer-events-none fixed right-0 bottom-0 left-[var(--admin-shell-content-left)] isolate z-10 flex flex-col transition-[left] duration-200 ease-linear',
94
94
  className
95
95
  )}
96
96
  >
97
97
  <ProgressiveBlur position="bottom" height="53px" />
98
- <div className="pointer-events-auto flex [--admin-shell-bottom-bar-height:53px] h-(--admin-shell-bottom-bar-height) items-center justify-between border-t border-border bg-background px-4 py-2">
98
+ <div className="pointer-events-auto flex h-(--admin-shell-bottom-bar-height) items-center justify-between border-t border-border bg-background px-4 py-2 [--admin-shell-bottom-bar-height:53px]">
99
99
  <div className="flex items-center gap-2">
100
100
  <span className="text-sm text-muted-foreground">Rows per page</span>
101
101
  <Select
102
102
  value={pageSize === -1 ? 'all' : String(pageSize)}
103
103
  onValueChange={handlePageSizeChange}
104
104
  >
105
- <SelectTrigger className="w-25 h-8">
105
+ <SelectTrigger className="h-8 w-25">
106
106
  <SelectValue />
107
107
  </SelectTrigger>
108
108
  <SelectContent>
@@ -154,7 +154,7 @@ export function DataTable<TData extends RowData, TValue>({
154
154
  <button
155
155
  type="button"
156
156
  aria-label={`Resize ${resizeLabel} column`}
157
- className="group/resize-handle absolute right-0 top-0 z-10 flex h-full w-5 cursor-col-resize touch-none select-none items-center justify-center text-foreground opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none group-focus-within/resize-header:opacity-100 data-[column-hovered=true]:opacity-100 data-[resizing=true]:opacity-100"
157
+ className="group/resize-handle absolute top-0 right-0 z-10 flex h-full w-5 cursor-col-resize touch-none items-center justify-center text-foreground opacity-0 transition-opacity select-none group-focus-within/resize-header:opacity-100 focus-visible:opacity-100 focus-visible:outline-none data-[column-hovered=true]:opacity-100 data-[resizing=true]:opacity-100"
158
158
  data-resize-handle
159
159
  data-column-hovered={
160
160
  hoveredColumnId === header.column.id ? 'true' : 'false'
@@ -36,7 +36,7 @@ export function DeleteDialog({
36
36
  <AlertDialog open={open} onOpenChange={setOpen}>
37
37
  <AlertDialogTrigger asChild>
38
38
  <Button variant="destructive" size="default">
39
- <Trash2 className="size-3.5 -ml-0.5" strokeWidth={2} />
39
+ <Trash2 className="-ml-0.5 size-3.5" strokeWidth={2} />
40
40
  Delete {count} {label}
41
41
  </Button>
42
42
  </AlertDialogTrigger>
@@ -35,10 +35,10 @@ export function CopyableCodeBlock({ label, code, language, tabs }: CopyableCodeB
35
35
  const hasTabs = codeTabs.length > 1
36
36
 
37
37
  return (
38
- <Card className="flex flex-col w-full p-0 gap-0">
38
+ <Card className="flex w-full flex-col gap-0 p-0">
39
39
  {hasTabs ? (
40
40
  <Tabs value={activeTab.id} onValueChange={setActiveTabId} className="min-h-0 flex-1 gap-0">
41
- <CardHeader className="flex items-center justify-between gap-2 pt-[5px]! pb-2! pl-3.5! pr-1 bg-background border-b border-border">
41
+ <CardHeader className="flex items-center justify-between gap-2 border-b border-border bg-background pt-[5px]! pr-1 pb-2! pl-3.5!">
42
42
  <div className="flex min-w-0 flex-1 items-center gap-2">
43
43
  <span className="min-w-0 truncate text-sm font-medium text-muted-foreground">
44
44
  {label}
@@ -66,7 +66,7 @@ export function CopyableCodeBlock({ label, code, language, tabs }: CopyableCodeB
66
66
 
67
67
  {codeTabs.map((tab) => (
68
68
  <TabsContent key={tab.id} value={tab.id} className="min-h-0 overflow-auto">
69
- <CardContent className="p-0 overflow-auto h-full">
69
+ <CardContent className="h-full overflow-auto p-0">
70
70
  <React.Suspense fallback={<PlainCodeFallback code={tab.code} />}>
71
71
  <LazyCodeMirror code={tab.code} language={tab.language} theme={codeTheme} />
72
72
  </React.Suspense>
@@ -76,7 +76,7 @@ export function CopyableCodeBlock({ label, code, language, tabs }: CopyableCodeB
76
76
  </Tabs>
77
77
  ) : (
78
78
  <>
79
- <CardHeader className="flex items-center justify-between gap-2 pt-[5px]! pb-2! pl-3.5! pr-1 bg-background border-b border-border">
79
+ <CardHeader className="flex items-center justify-between gap-2 border-b border-border bg-background pt-[5px]! pr-1 pb-2! pl-3.5!">
80
80
  <span className="min-w-0 truncate text-sm font-medium text-muted-foreground">
81
81
  {label}
82
82
  </span>
@@ -92,7 +92,7 @@ export function CopyableCodeBlock({ label, code, language, tabs }: CopyableCodeB
92
92
  {isCopied ? <Check /> : <Copy />}
93
93
  </Button>
94
94
  </CardHeader>
95
- <CardContent className="p-0 overflow-auto">
95
+ <CardContent className="overflow-auto p-0">
96
96
  <React.Suspense fallback={<PlainCodeFallback code={code} />}>
97
97
  <LazyCodeMirror code={code} language={language} theme={codeTheme} />
98
98
  </React.Suspense>
@@ -4,7 +4,7 @@ interface PlainCodeFallbackProps {
4
4
 
5
5
  export function PlainCodeFallback({ code }: PlainCodeFallbackProps) {
6
6
  return (
7
- <pre className="min-h-full overflow-x-auto whitespace-pre p-3 font-mono text-xs leading-5 text-foreground">
7
+ <pre className="min-h-full overflow-x-auto p-3 font-mono text-xs leading-5 whitespace-pre text-foreground">
8
8
  {code}
9
9
  </pre>
10
10
  )
@@ -62,7 +62,7 @@ export function DevModeIntegrateButton({
62
62
  </DrawerHeader>
63
63
 
64
64
  {loadedIntegration ? (
65
- <Tabs defaultValue="snippets" className="min-h-0 flex-1 overflow-hidden px-6 pb-4 gap-4">
65
+ <Tabs defaultValue="snippets" className="min-h-0 flex-1 gap-4 overflow-hidden px-6 pb-4">
66
66
  <TabsList className="w-full">
67
67
  <TabsTrigger value="snippets">Snippets</TabsTrigger>
68
68
  <TabsTrigger value="lifecycle-hooks">Lifecycle Hooks</TabsTrigger>
@@ -64,13 +64,13 @@ export function EntityFiltersBar({
64
64
  return (
65
65
  <div
66
66
  className={cn(
67
- 'sticky z-10 flex gap-1 px-4 bg-background h-16 xl:flex-row xl:items-center xl:justify-between top-14 transition-shadow overflow-x-clip',
67
+ 'sticky top-14 z-10 flex h-16 gap-1 overflow-x-clip bg-background px-4 transition-shadow xl:flex-row xl:items-center xl:justify-between',
68
68
  {
69
69
  'shadow-modal/3': hasScrolled
70
70
  }
71
71
  )}
72
72
  >
73
- <div className="flex flex-1 items-center gap-1 w-full">
73
+ <div className="flex w-full flex-1 items-center gap-1">
74
74
  {onSearch ? (
75
75
  <SearchInput
76
76
  value={search}
@@ -102,7 +102,7 @@ export function EntityFiltersBar({
102
102
  }
103
103
 
104
104
  return (
105
- <div key={filter.key} className="flex items-center gap-1 relative">
105
+ <div key={filter.key} className="relative flex items-center gap-1">
106
106
  <DropdownMenu>
107
107
  <DropdownMenuTrigger asChild>
108
108
  <Button variant="outline" className="w-45 justify-between">
@@ -112,7 +112,7 @@ export function EntityFiltersBar({
112
112
  : `All ${filter.label}`}
113
113
 
114
114
  {!filter.value && (
115
- <ChevronDown className="size-4 absolute right-2.5 top-1/2 -translate-y-1/2 text-foreground" />
115
+ <ChevronDown className="absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-foreground" />
116
116
  )}
117
117
  </Button>
118
118
  </DropdownMenuTrigger>
@@ -144,7 +144,7 @@ export function EntityFiltersBar({
144
144
  type="button"
145
145
  variant="ghost"
146
146
  size="icon-sm"
147
- className="absolute right-1 top-1/2 -translate-y-1/2 text-foreground size-7"
147
+ className="absolute top-1/2 right-1 size-7 -translate-y-1/2 text-foreground"
148
148
  onClick={() => {
149
149
  React.startTransition(() => {
150
150
  onFilterChange({ [filter.key]: null })
@@ -54,7 +54,7 @@ export function EntityMetadata({
54
54
  <span className="text-foreground">{data.id}</span>
55
55
  </div>
56
56
 
57
- <Separator orientation="vertical" className="h-4 rotate-20 my-auto" />
57
+ <Separator orientation="vertical" className="my-auto h-4 rotate-20" />
58
58
 
59
59
  <div className="flex items-center gap-1">
60
60
  <span>Created by</span>
@@ -63,7 +63,7 @@ export function EntityMetadata({
63
63
  <span className="text-foreground">{format(created, fullFmt)}</span>
64
64
  </div>
65
65
 
66
- <Separator orientation="vertical" className="h-4 rotate-20 my-auto" />
66
+ <Separator orientation="vertical" className="my-auto h-4 rotate-20" />
67
67
 
68
68
  <div className="flex items-center gap-1">
69
69
  <span>Last edited by</span>