@tuturuuu/ui 0.6.2 → 0.8.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 (108) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/biome.json +1 -1
  3. package/package.json +11 -11
  4. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  5. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  6. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  7. package/src/components/ui/calendar.test.tsx +24 -0
  8. package/src/components/ui/calendar.tsx +1 -0
  9. package/src/components/ui/currency-input.test.tsx +43 -0
  10. package/src/components/ui/currency-input.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  12. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  13. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  14. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  15. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  16. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  17. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  18. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  19. package/src/components/ui/date-time-picker.tsx +352 -234
  20. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  21. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  22. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  23. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  24. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  25. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  26. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  27. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  28. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  29. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  30. package/src/components/ui/finance/transactions/form-types.ts +5 -0
  31. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  32. package/src/components/ui/finance/transactions/form.tsx +116 -20
  33. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  34. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  35. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  36. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  37. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  38. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  39. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  40. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  41. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  42. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  43. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  48. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  49. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  50. package/src/components/ui/money-input.test.tsx +64 -0
  51. package/src/components/ui/money-input.tsx +63 -0
  52. package/src/components/ui/optional-time-picker.tsx +95 -0
  53. package/src/components/ui/quick-command-center.test.tsx +90 -0
  54. package/src/components/ui/quick-command-center.tsx +190 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +126 -50
  56. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +23 -20
  58. package/src/components/ui/storefront/image-panel.tsx +6 -0
  59. package/src/components/ui/storefront/index.ts +11 -0
  60. package/src/components/ui/storefront/listing-card.tsx +84 -22
  61. package/src/components/ui/storefront/product-detail.tsx +289 -0
  62. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  63. package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
  64. package/src/components/ui/storefront/storefront-surface.tsx +371 -128
  65. package/src/components/ui/storefront/types.ts +25 -1
  66. package/src/components/ui/storefront/utils.ts +118 -13
  67. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  68. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  69. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  70. package/src/components/ui/text-editor/content-migration.ts +41 -18
  71. package/src/components/ui/text-editor/extensions.ts +1 -1
  72. package/src/components/ui/text-editor/image-extension.ts +40 -18
  73. package/src/components/ui/text-editor/video-extension.ts +11 -2
  74. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  75. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  76. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  79. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  80. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  81. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  84. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  85. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  86. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  87. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  88. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  89. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  90. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  91. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  92. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  93. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  94. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  95. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  100. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  101. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
  102. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  103. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  104. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  105. package/src/hooks/useBoardRealtime.ts +6 -3
  106. package/src/hooks/useBoardRealtime.types.ts +11 -0
  107. package/src/hooks/useCursorTracking.ts +91 -27
  108. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -5,6 +5,7 @@ import {
5
5
  ShieldUser,
6
6
  User as UserIcon,
7
7
  UserMinus,
8
+ X,
8
9
  } from '@tuturuuu/icons';
9
10
  import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
10
11
  import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
@@ -26,6 +27,13 @@ import {
26
27
  } from './member-filter-utils';
27
28
  import type { WorkspaceAccessLabels, WorkspaceAccessRole } from './types';
28
29
 
30
+ type GuestContext = InternalApiEnhancedWorkspaceMember & {
31
+ direct_board_guest?: boolean;
32
+ guest_board_count?: number;
33
+ guest_board_names?: string[];
34
+ guest_highest_permission?: 'edit' | 'view' | null;
35
+ };
36
+
29
37
  type Props = {
30
38
  canManageMembers: boolean;
31
39
  canManageRoles: boolean;
@@ -57,191 +65,192 @@ export function WorkspaceAccessMemberRow({
57
65
  const memberId = member.id ?? null;
58
66
  const availableRoles = getAvailableRolesForMember(member, roles);
59
67
  const memberName = getMemberDisplayName(member, t('common.unknown'));
60
- const guestContext = member as InternalApiEnhancedWorkspaceMember & {
61
- direct_board_guest?: boolean;
62
- guest_board_count?: number;
63
- guest_board_names?: string[];
64
- guest_highest_permission?: 'edit' | 'view' | null;
65
- };
68
+ const guest = member as GuestContext;
69
+ const canRemoveRoles = canManageRoles && memberId && !member.pending;
70
+
71
+ const accent = member.pending
72
+ ? 'before:bg-dynamic-orange'
73
+ : guest.direct_board_guest || member.workspace_member_type === 'GUEST'
74
+ ? 'before:bg-dynamic-blue'
75
+ : member.is_creator
76
+ ? 'before:bg-dynamic-yellow'
77
+ : 'before:bg-transparent';
66
78
 
67
79
  return (
68
80
  <article
69
- className={`relative rounded-lg border p-4 transition-colors ${
70
- member.pending
71
- ? 'border-dashed bg-transparent'
72
- : 'bg-primary-foreground/20 hover:bg-primary-foreground/30'
73
- }`}
81
+ className={`group relative overflow-hidden rounded-xl border bg-background pl-5 transition-all before:absolute before:inset-y-0 before:left-0 before:w-1 hover:border-foreground/20 hover:shadow-sm ${accent} ${member.pending ? 'border-dashed' : ''}`}
74
82
  >
75
- <div className="flex items-start gap-3">
76
- <Avatar>
77
- <AvatarImage src={member.avatar_url ?? undefined} />
78
- <AvatarFallback className="font-semibold">
79
- {member.display_name ? (
80
- getInitials(member.display_name)
81
- ) : (
82
- <UserIcon className="h-5 w-5" />
83
- )}
84
- </AvatarFallback>
85
- </Avatar>
86
-
87
- <div className="min-w-0 flex-1 space-y-2">
88
- <div className="flex flex-wrap items-center gap-1.5">
89
- <p className="truncate font-semibold lg:text-lg">{memberName}</p>
90
- {member.is_creator ? (
91
- <Badge className="h-5 gap-1 border-dynamic-yellow/50 bg-dynamic-yellow/10 px-1.5 text-dynamic-yellow text-xs">
92
- <Crown className="h-3 w-3" />
93
- {t('ws-members.creator_badge')}
94
- </Badge>
95
- ) : null}
96
- {member.pending ? (
97
- <Badge variant="outline" className="h-5 px-1.5 text-xs">
98
- {t('ws-members.invited')}
99
- </Badge>
100
- ) : null}
101
- {member.workspace_member_type === 'GUEST' ? (
102
- <Badge className="h-5 border-foreground/20 bg-foreground/5 px-1.5 text-foreground/80 text-xs">
103
- {guestContext.direct_board_guest
104
- ? t('ws-members.direct_board_guest_badge')
105
- : t('ws-members.guest_badge')}
106
- </Badge>
107
- ) : null}
108
- {guestContext.direct_board_guest ? (
109
- <Badge className="h-5 border-dynamic-blue/50 bg-dynamic-blue/10 px-1.5 text-dynamic-blue text-xs">
110
- {guestContext.guest_highest_permission === 'edit'
111
- ? t('ws-members.board_guest_can_edit')
112
- : t('ws-members.board_guest_can_view')}
113
- </Badge>
114
- ) : null}
115
- </div>
116
-
117
- <p className="truncate font-semibold text-muted-foreground text-sm">
118
- {member.email ||
119
- (member.handle ? `@${member.handle}` : t('common.unknown'))}
120
- </p>
121
- </div>
122
- </div>
83
+ <div className="p-4 pl-0">
84
+ <div className="flex items-start gap-3">
85
+ <Avatar className="h-11 w-11 border border-border">
86
+ <AvatarImage src={member.avatar_url ?? undefined} />
87
+ <AvatarFallback className="font-semibold">
88
+ {member.display_name ? (
89
+ getInitials(member.display_name)
90
+ ) : (
91
+ <UserIcon className="h-5 w-5" />
92
+ )}
93
+ </AvatarFallback>
94
+ </Avatar>
123
95
 
124
- <div className="mt-3 flex flex-wrap gap-2">
125
- {member.roles.length > 0 ? (
126
- member.roles.map((role) => (
127
- <Badge
128
- key={`${member.id ?? member.email}-${role.id}`}
129
- className="h-5 gap-1 border-dynamic-purple/50 bg-dynamic-purple/10 px-1.5 text-dynamic-purple text-xs"
130
- >
131
- <span>{role.name}</span>
132
- {canManageRoles && memberId && !member.pending ? (
133
- <button
134
- type="button"
135
- className="rounded-full px-1 hover:bg-foreground/10"
136
- onClick={() =>
137
- onRemoveRole({
138
- roleId: role.id,
139
- userId: memberId,
140
- })
141
- }
142
- aria-label={t('common.delete')}
143
- >
144
- x
145
- </button>
96
+ <div className="min-w-0 flex-1">
97
+ <div className="flex flex-wrap items-center gap-1.5">
98
+ <p className="truncate font-semibold text-base">{memberName}</p>
99
+ {member.is_creator ? (
100
+ <Badge className="h-5 gap-1 border-dynamic-yellow/50 bg-dynamic-yellow/10 px-1.5 text-dynamic-yellow text-xs">
101
+ <Crown className="h-3 w-3" />
102
+ {t('ws-members.creator_badge')}
103
+ </Badge>
146
104
  ) : null}
147
- </Badge>
148
- ))
149
- ) : (
150
- <Badge variant="outline" className="h-5 px-1.5 text-xs">
151
- {labels.noRolesLabel}
152
- </Badge>
153
- )}
154
- </div>
155
-
156
- {guestContext.direct_board_guest ? (
157
- <div className="mt-3 rounded-md border border-dynamic-blue/20 bg-dynamic-blue/5 p-3 text-sm">
158
- <div className="font-medium text-dynamic-blue">
159
- {t('ws-members.direct_board_guest_scope_title')}
160
- </div>
161
- <p className="mt-1 text-muted-foreground">
162
- {t('ws-members.direct_board_guest_scope_description', {
163
- count: guestContext.guest_board_count ?? 0,
164
- })}
165
- </p>
166
- {guestContext.guest_board_names?.length ? (
167
- <div className="mt-2 flex flex-wrap gap-1">
168
- {guestContext.guest_board_names.slice(0, 4).map((name) => (
169
- <Badge key={name} variant="outline" className="text-xs">
170
- {name}
105
+ {member.pending ? (
106
+ <Badge className="h-5 border-dynamic-orange/50 bg-dynamic-orange/10 px-1.5 text-dynamic-orange text-xs">
107
+ {t('ws-members.invited')}
171
108
  </Badge>
172
- ))}
173
- {guestContext.guest_board_names.length > 4 ? (
174
- <Badge variant="outline" className="text-xs">
175
- +{guestContext.guest_board_names.length - 4}
109
+ ) : null}
110
+ {member.workspace_member_type === 'GUEST' ? (
111
+ <Badge className="h-5 border-foreground/20 bg-foreground/5 px-1.5 text-foreground/80 text-xs">
112
+ {guest.direct_board_guest
113
+ ? t('ws-members.direct_board_guest_badge')
114
+ : t('ws-members.guest_badge')}
115
+ </Badge>
116
+ ) : null}
117
+ {guest.direct_board_guest ? (
118
+ <Badge className="h-5 border-dynamic-blue/50 bg-dynamic-blue/10 px-1.5 text-dynamic-blue text-xs">
119
+ {guest.guest_highest_permission === 'edit'
120
+ ? t('ws-members.board_guest_can_edit')
121
+ : t('ws-members.board_guest_can_view')}
176
122
  </Badge>
177
123
  ) : null}
178
124
  </div>
179
- ) : null}
125
+ <p className="mt-0.5 truncate text-muted-foreground text-sm">
126
+ {member.email ||
127
+ (member.handle ? `@${member.handle}` : t('common.unknown'))}
128
+ </p>
129
+ </div>
180
130
  </div>
181
- ) : null}
182
131
 
183
- <div className="mt-3 flex flex-col gap-3 border-t pt-3 text-sm md:flex-row md:items-center md:justify-between">
184
- {member.created_at ? (
185
- <div className="text-muted-foreground">
186
- <span>
187
- {t(
188
- member.pending
189
- ? 'ws-members.invited'
190
- : 'ws-members.member_since'
191
- )}
192
- </span>{' '}
193
- <span className="font-semibold text-foreground">
194
- {moment(member.created_at).locale(locale).fromNow()}
195
- </span>
132
+ <div className="mt-3 flex flex-wrap gap-1.5">
133
+ {member.roles.length > 0 ? (
134
+ member.roles.map((role) => (
135
+ <Badge
136
+ key={`${member.id ?? member.email}-${role.id}`}
137
+ className="h-5 gap-1 border-dynamic-purple/40 bg-dynamic-purple/10 px-1.5 text-dynamic-purple text-xs"
138
+ >
139
+ <span>{role.name}</span>
140
+ {canRemoveRoles ? (
141
+ <button
142
+ type="button"
143
+ className="rounded-full p-0.5 transition-colors hover:bg-dynamic-purple/20"
144
+ onClick={() =>
145
+ onRemoveRole({ roleId: role.id, userId: memberId })
146
+ }
147
+ aria-label={t('common.delete')}
148
+ >
149
+ <X className="h-3 w-3" />
150
+ </button>
151
+ ) : null}
152
+ </Badge>
153
+ ))
154
+ ) : (
155
+ <Badge variant="outline" className="h-5 px-1.5 text-xs">
156
+ {labels.noRolesLabel}
157
+ </Badge>
158
+ )}
159
+ </div>
160
+
161
+ {guest.direct_board_guest ? (
162
+ <div className="mt-3 rounded-lg border border-dynamic-blue/20 bg-dynamic-blue/5 p-3 text-sm">
163
+ <div className="font-medium text-dynamic-blue">
164
+ {t('ws-members.direct_board_guest_scope_title')}
165
+ </div>
166
+ <p className="mt-1 text-muted-foreground">
167
+ {t('ws-members.direct_board_guest_scope_description', {
168
+ count: guest.guest_board_count ?? 0,
169
+ })}
170
+ </p>
171
+ {guest.guest_board_names?.length ? (
172
+ <div className="mt-2 flex flex-wrap gap-1">
173
+ {guest.guest_board_names.slice(0, 4).map((name) => (
174
+ <Badge key={name} variant="outline" className="text-xs">
175
+ {name}
176
+ </Badge>
177
+ ))}
178
+ {guest.guest_board_names.length > 4 ? (
179
+ <Badge variant="outline" className="text-xs">
180
+ +{guest.guest_board_names.length - 4}
181
+ </Badge>
182
+ ) : null}
183
+ </div>
184
+ ) : null}
196
185
  </div>
197
186
  ) : null}
198
187
 
199
- <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
200
- {canManageRoles && memberId && !member.pending ? (
201
- <Select
202
- onValueChange={(roleId) =>
203
- onAssignRole({ roleId, userId: memberId })
204
- }
205
- >
206
- <SelectTrigger className="min-w-[180px]">
207
- <SelectValue placeholder={labels.assignRolePlaceholder} />
208
- </SelectTrigger>
209
- <SelectContent>
210
- {availableRoles.length === 0 ? (
211
- <SelectItem value="__no_roles__" disabled>
212
- {labels.noAdditionalRoles}
213
- </SelectItem>
214
- ) : (
215
- availableRoles.map((role) => (
216
- <SelectItem key={role.id} value={role.id}>
217
- {role.name}
218
- </SelectItem>
219
- ))
188
+ <div className="mt-3 flex flex-col gap-3 border-border border-t pt-3 text-sm md:flex-row md:items-center md:justify-between">
189
+ {member.created_at ? (
190
+ <div className="text-muted-foreground text-xs">
191
+ <span>
192
+ {t(
193
+ member.pending
194
+ ? 'ws-members.invited'
195
+ : 'ws-members.member_since'
220
196
  )}
221
- </SelectContent>
222
- </Select>
223
- ) : null}
224
-
225
- {canManageMembers && !member.is_creator ? (
226
- <Button
227
- variant="outline"
228
- disabled={isMutating}
229
- onClick={() =>
230
- onRemoveMember({
231
- email: member.email,
232
- userId: member.pending ? null : member.id,
233
- })
234
- }
235
- >
236
- <UserMinus className="mr-2 h-4 w-4" />
237
- {labels.removeMemberAction}
238
- </Button>
239
- ) : (
240
- <div className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-muted-foreground text-sm">
241
- <ShieldUser className="h-4 w-4" />
242
- {labels.protectedMemberLabel}
197
+ </span>{' '}
198
+ <span className="font-semibold text-foreground">
199
+ {moment(member.created_at).locale(locale).fromNow()}
200
+ </span>
243
201
  </div>
202
+ ) : (
203
+ <span />
244
204
  )}
205
+
206
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
207
+ {canRemoveRoles ? (
208
+ <Select
209
+ onValueChange={(roleId) =>
210
+ onAssignRole({ roleId, userId: memberId })
211
+ }
212
+ >
213
+ <SelectTrigger className="h-9 min-w-[180px]">
214
+ <SelectValue placeholder={labels.assignRolePlaceholder} />
215
+ </SelectTrigger>
216
+ <SelectContent>
217
+ {availableRoles.length === 0 ? (
218
+ <SelectItem value="__no_roles__" disabled>
219
+ {labels.noAdditionalRoles}
220
+ </SelectItem>
221
+ ) : (
222
+ availableRoles.map((role) => (
223
+ <SelectItem key={role.id} value={role.id}>
224
+ {role.name}
225
+ </SelectItem>
226
+ ))
227
+ )}
228
+ </SelectContent>
229
+ </Select>
230
+ ) : null}
231
+
232
+ {canManageMembers && !member.is_creator ? (
233
+ <Button
234
+ variant="outline"
235
+ size="sm"
236
+ disabled={isMutating}
237
+ onClick={() =>
238
+ onRemoveMember({
239
+ email: member.email,
240
+ userId: member.pending ? null : member.id,
241
+ })
242
+ }
243
+ >
244
+ <UserMinus className="mr-2 h-4 w-4" />
245
+ {labels.removeMemberAction}
246
+ </Button>
247
+ ) : (
248
+ <div className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-muted-foreground text-xs">
249
+ <ShieldUser className="h-3.5 w-3.5" />
250
+ {labels.protectedMemberLabel}
251
+ </div>
252
+ )}
253
+ </div>
245
254
  </div>
246
255
  </div>
247
256
  </article>
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { UsersRound } from '@tuturuuu/icons';
3
4
  import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
4
5
  import { Skeleton } from '@tuturuuu/ui/skeleton';
5
6
  import { useTranslations } from 'next-intl';
@@ -43,22 +44,27 @@ export function WorkspaceAccessMembers({
43
44
  if (isLoading) {
44
45
  return (
45
46
  <div className="grid gap-4 lg:grid-cols-2">
46
- <Skeleton className="h-52 rounded-lg" />
47
- <Skeleton className="h-52 rounded-lg" />
48
- <Skeleton className="h-52 rounded-lg" />
49
- <Skeleton className="h-52 rounded-lg" />
47
+ <Skeleton className="h-48 rounded-xl" />
48
+ <Skeleton className="h-48 rounded-xl" />
49
+ <Skeleton className="h-48 rounded-xl" />
50
+ <Skeleton className="h-48 rounded-xl" />
50
51
  </div>
51
52
  );
52
53
  }
53
54
 
54
55
  if (members.length === 0) {
55
56
  return (
56
- <div className="flex min-h-56 items-center justify-center rounded-lg border border-dashed p-6 text-center text-muted-foreground text-sm">
57
- {searchTerm.trim()
58
- ? t('ws-members.no_members_match')
59
- : status === 'invited'
60
- ? t('ws-members.no_invited_members_found')
61
- : t('ws-members.no_members_found')}
57
+ <div className="flex min-h-56 flex-col items-center justify-center gap-3 rounded-xl border border-border border-dashed p-8 text-center">
58
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-foreground/5 text-muted-foreground">
59
+ <UsersRound className="h-6 w-6" />
60
+ </div>
61
+ <p className="text-muted-foreground text-sm">
62
+ {searchTerm.trim()
63
+ ? t('ws-members.no_members_match')
64
+ : status === 'invited'
65
+ ? t('ws-members.no_invited_members_found')
66
+ : t('ws-members.no_members_found')}
67
+ </p>
62
68
  </div>
63
69
  );
64
70
  }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { ShieldCheck, Users } from '@tuturuuu/icons';
4
- import { Badge } from '@tuturuuu/ui/badge';
3
+ import { ShieldCheck, UserCheck, UserPlus, Users } from '@tuturuuu/icons';
5
4
  import { useTranslations } from 'next-intl';
5
+ import type { ReactNode } from 'react';
6
6
  import type { WorkspaceAccessContext, WorkspaceAccessMode } from './types';
7
7
 
8
8
  type Props = {
@@ -13,6 +13,13 @@ type Props = {
13
13
  totalCount: number;
14
14
  };
15
15
 
16
+ type Stat = {
17
+ accent: string;
18
+ icon: ReactNode;
19
+ label: string;
20
+ value: number;
21
+ };
22
+
16
23
  export function WorkspaceAccessPageHeader({
17
24
  context,
18
25
  invitedCount,
@@ -22,49 +29,81 @@ export function WorkspaceAccessPageHeader({
22
29
  }: Props) {
23
30
  const t = useTranslations() as (key: string) => string;
24
31
 
32
+ const stats: Stat[] = [
33
+ {
34
+ accent: 'text-dynamic-blue',
35
+ icon: <Users className="h-4 w-4" />,
36
+ label: t('common.total'),
37
+ value: totalCount,
38
+ },
39
+ {
40
+ accent: 'text-dynamic-green',
41
+ icon: <UserCheck className="h-4 w-4" />,
42
+ label: t('ws-members.active_members'),
43
+ value: joinedCount,
44
+ },
45
+ {
46
+ accent: 'text-dynamic-orange',
47
+ icon: <UserPlus className="h-4 w-4" />,
48
+ label: t('ws-members.pending_invitations'),
49
+ value: invitedCount,
50
+ },
51
+ ];
52
+
25
53
  return (
26
- <section className="overflow-hidden rounded-xl border border-border bg-linear-to-br from-background via-background to-foreground/[0.02] p-6 shadow-sm">
27
- <div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
28
- <div className="min-w-0">
29
- <div className="flex items-center gap-3">
30
- <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-linear-to-br from-dynamic-blue to-dynamic-purple shadow-lg">
31
- <Users className="h-6 w-6 text-background" />
32
- </div>
33
- <div className="min-w-0">
34
- <h1 className="bg-linear-to-br from-foreground to-foreground/70 bg-clip-text font-bold text-3xl text-transparent tracking-tight">
54
+ <section className="rounded-xl border border-border bg-background">
55
+ <div className="flex flex-col gap-6 p-6 lg:flex-row lg:items-start lg:justify-between">
56
+ <div className="flex min-w-0 gap-4">
57
+ <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-dynamic-blue/30 bg-dynamic-blue/10 text-dynamic-blue">
58
+ <Users className="h-5 w-5" />
59
+ </div>
60
+ <div className="min-w-0">
61
+ <div className="flex items-center gap-2">
62
+ <h1 className="truncate font-bold text-2xl tracking-tight">
35
63
  {mode === 'cms'
36
64
  ? t('external-projects.settings.members_title')
37
65
  : t('workspace-settings-layout.members')}
38
66
  </h1>
39
- {context.boundProjectName ? (
40
- <p className="mt-1 text-muted-foreground text-sm">
41
- {t('external-projects.settings.bound_project_label')}:{' '}
42
- {context.boundProjectName}
43
- </p>
44
- ) : null}
67
+ <span className="inline-flex items-center gap-1 rounded-full border border-dynamic-purple/30 bg-dynamic-purple/10 px-2 py-0.5 font-medium text-dynamic-purple text-xs">
68
+ <ShieldCheck className="h-3 w-3" />
69
+ {t('ws-roles.plural')}
70
+ </span>
45
71
  </div>
72
+ <p className="mt-1.5 max-w-2xl text-muted-foreground text-sm leading-6">
73
+ {mode === 'cms'
74
+ ? t('external-projects.settings.members_description')
75
+ : t('ws-members.description')}
76
+ </p>
77
+ {context.boundProjectName ? (
78
+ <p className="mt-2 inline-flex items-center gap-1.5 rounded-md border border-border bg-foreground/[0.03] px-2 py-1 text-muted-foreground text-xs">
79
+ {t('external-projects.settings.bound_project_label')}:
80
+ <span className="font-semibold text-foreground">
81
+ {context.boundProjectName}
82
+ </span>
83
+ </p>
84
+ ) : null}
46
85
  </div>
47
- <p className="mt-4 max-w-3xl text-muted-foreground text-sm leading-6">
48
- {mode === 'cms'
49
- ? t('external-projects.settings.members_description')
50
- : t('ws-members.description')}
51
- </p>
52
86
  </div>
53
87
 
54
- <div className="flex flex-wrap gap-2 lg:justify-end">
55
- <Badge variant="outline" className="rounded-full">
56
- {t('common.total')}: {totalCount}
57
- </Badge>
58
- <Badge variant="outline" className="rounded-full">
59
- {t('ws-members.active_members')}: {joinedCount}
60
- </Badge>
61
- <Badge variant="outline" className="rounded-full">
62
- {t('ws-members.pending_invitations')}: {invitedCount}
63
- </Badge>
64
- <Badge variant="secondary" className="gap-1 rounded-full">
65
- <ShieldCheck className="h-3.5 w-3.5" />
66
- {t('ws-roles.plural')}
67
- </Badge>
88
+ <div className="grid shrink-0 grid-cols-3 divide-x divide-border overflow-hidden rounded-lg border border-border bg-foreground/[0.02]">
89
+ {stats.map((stat) => (
90
+ <div
91
+ key={stat.label}
92
+ className="flex flex-col gap-1 px-4 py-3 text-center"
93
+ >
94
+ <span
95
+ className={`inline-flex items-center justify-center gap-1.5 ${stat.accent}`}
96
+ >
97
+ {stat.icon}
98
+ <span className="font-bold text-foreground text-xl tabular-nums">
99
+ {stat.value}
100
+ </span>
101
+ </span>
102
+ <span className="text-muted-foreground text-xs">
103
+ {stat.label}
104
+ </span>
105
+ </div>
106
+ ))}
68
107
  </div>
69
108
  </div>
70
109
  </section>