@tuturuuu/ui 0.7.0 → 0.9.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -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>
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
- import { Settings } from '@tuturuuu/icons';
4
+ import { TriangleAlert } from '@tuturuuu/icons';
5
5
  import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
6
6
  import type { WorkspaceDefaultPermissionMemberType } from '@tuturuuu/types';
7
7
  import { Skeleton } from '@tuturuuu/ui/skeleton';
@@ -222,10 +222,10 @@ export function WorkspaceAccessPage({
222
222
 
223
223
  if (contextQuery.isPending) {
224
224
  return (
225
- <div className="space-y-4">
226
- <Skeleton className="h-28 rounded-lg" />
227
- <Skeleton className="h-12 rounded-lg" />
228
- <Skeleton className="h-96 rounded-lg" />
225
+ <div className="space-y-6">
226
+ <Skeleton className="h-32 rounded-xl" />
227
+ <Skeleton className="h-10 rounded-xl" />
228
+ <Skeleton className="h-96 rounded-xl" />
229
229
  </div>
230
230
  );
231
231
  }
@@ -255,42 +255,39 @@ export function WorkspaceAccessPage({
255
255
  search={search}
256
256
  />
257
257
 
258
- <TabsContent value="people" className="mt-4">
259
- <div className="rounded-xl border border-border bg-background p-6 shadow-sm">
260
- <WorkspaceAccessPeopleFilters
261
- filterOptions={filterOptions}
262
- filters={filters}
263
- labels={labels}
264
- onFiltersChange={setFilters}
265
- onStatusChange={setStatus}
266
- status={status}
267
- />
258
+ <TabsContent value="people" className="mt-6 space-y-4">
259
+ <WorkspaceAccessPeopleFilters
260
+ filterOptions={filterOptions}
261
+ filters={filters}
262
+ labels={labels}
263
+ onFiltersChange={setFilters}
264
+ onStatusChange={setStatus}
265
+ status={status}
266
+ />
268
267
 
269
- <WorkspaceAccessMembers
270
- canManageMembers={canManageMembers}
271
- canManageRoles={canManageRoles}
272
- isLoading={membersQuery.isPending}
273
- isMutating={
274
- removeMemberMutation.isPending ||
275
- roleMembershipMutation.isPending
276
- }
277
- labels={labels}
278
- members={visibleMembers}
279
- onAssignRole={(payload) =>
280
- roleMembershipMutation.mutate({ ...payload, action: 'add' })
281
- }
282
- onRemoveMember={(payload) => removeMemberMutation.mutate(payload)}
283
- onRemoveRole={(payload) =>
284
- roleMembershipMutation.mutate({ ...payload, action: 'remove' })
285
- }
286
- roles={roles}
287
- searchTerm={search}
288
- status={status}
289
- />
290
- </div>
268
+ <WorkspaceAccessMembers
269
+ canManageMembers={canManageMembers}
270
+ canManageRoles={canManageRoles}
271
+ isLoading={membersQuery.isPending}
272
+ isMutating={
273
+ removeMemberMutation.isPending || roleMembershipMutation.isPending
274
+ }
275
+ labels={labels}
276
+ members={visibleMembers}
277
+ onAssignRole={(payload) =>
278
+ roleMembershipMutation.mutate({ ...payload, action: 'add' })
279
+ }
280
+ onRemoveMember={(payload) => removeMemberMutation.mutate(payload)}
281
+ onRemoveRole={(payload) =>
282
+ roleMembershipMutation.mutate({ ...payload, action: 'remove' })
283
+ }
284
+ roles={roles}
285
+ searchTerm={search}
286
+ status={status}
287
+ />
291
288
  </TabsContent>
292
289
 
293
- <TabsContent value="roles" className="mt-4">
290
+ <TabsContent value="roles" className="mt-6">
294
291
  <WorkspaceAccessRoles
295
292
  canManageRoles={canManageRoles}
296
293
  isLoading={rolesQuery.isPending}
@@ -309,7 +306,7 @@ export function WorkspaceAccessPage({
309
306
  />
310
307
  </TabsContent>
311
308
 
312
- <TabsContent value="defaults-member" className="mt-4">
309
+ <TabsContent value="defaults-member" className="mt-6">
313
310
  <WorkspaceAccessDefaultRoleCard
314
311
  canManageRoles={canManageRoles}
315
312
  isLoading={memberDefaultQuery.isPending}
@@ -327,7 +324,7 @@ export function WorkspaceAccessPage({
327
324
  />
328
325
  </TabsContent>
329
326
 
330
- <TabsContent value="defaults-guest" className="mt-4">
327
+ <TabsContent value="defaults-guest" className="mt-6">
331
328
  <WorkspaceAccessDefaultRoleCard
332
329
  canManageRoles={canManageRoles}
333
330
  isLoading={guestDefaultQuery.isPending}
@@ -378,8 +375,8 @@ export function WorkspaceAccessPage({
378
375
  ) : null}
379
376
 
380
377
  {contextQuery.isError ? (
381
- <div className="rounded-lg border border-dynamic-red/20 bg-dynamic-red/5 p-4 text-dynamic-red text-sm">
382
- <Settings className="mr-2 inline h-4 w-4" />
378
+ <div className="flex items-center gap-2 rounded-xl border border-dynamic-red/20 bg-dynamic-red/5 p-4 text-dynamic-red text-sm">
379
+ <TriangleAlert className="h-4 w-4 shrink-0" />
383
380
  {t('common.error')}
384
381
  </div>
385
382
  ) : null}
@@ -52,7 +52,7 @@ export function WorkspaceAccessPeopleFilters({
52
52
  filters.permissionIds.length + filters.roleIds.length;
53
53
 
54
54
  return (
55
- <div className="mb-5 flex flex-col gap-3 rounded-lg border border-border bg-foreground/[0.02] p-3 sm:flex-row sm:items-center sm:justify-between">
55
+ <div className="flex flex-col gap-3 rounded-xl border border-border bg-foreground/[0.02] p-3 sm:flex-row sm:items-center sm:justify-between">
56
56
  <div className="flex flex-wrap items-center gap-2">
57
57
  <div className="mr-1 flex items-center gap-2 text-muted-foreground text-sm">
58
58
  <ListFilter className="h-4 w-4" />
@@ -1,16 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { Pencil, Plus, Trash2 } from '@tuturuuu/icons';
3
+ import { Pencil, Plus, ShieldCheck, Trash2, Users } from '@tuturuuu/icons';
4
4
  import type { InternalApiEnhancedWorkspaceMember } from '@tuturuuu/types';
5
5
  import { Badge } from '@tuturuuu/ui/badge';
6
6
  import { Button } from '@tuturuuu/ui/button';
7
- import {
8
- Card,
9
- CardContent,
10
- CardDescription,
11
- CardHeader,
12
- CardTitle,
13
- } from '@tuturuuu/ui/card';
14
7
  import { Skeleton } from '@tuturuuu/ui/skeleton';
15
8
  import { useTranslations } from 'next-intl';
16
9
  import { getMemberDisplayName } from './member-filter-utils';
@@ -56,37 +49,36 @@ export function WorkspaceAccessRoles({
56
49
 
57
50
  return (
58
51
  <div className="space-y-4">
59
- <Card className="shadow-none">
60
- <CardHeader>
61
- <div className="flex flex-wrap items-start justify-between gap-4">
62
- <div>
63
- <CardTitle>{labels.accessLevelsLabel}</CardTitle>
64
- <CardDescription>{t('ws-roles.description')}</CardDescription>
65
- </div>
66
- {canManageRoles ? (
67
- <Button onClick={onCreateRole}>
68
- <Plus className="mr-2 h-4 w-4" />
69
- {t('ws-roles.create')}
70
- </Button>
71
- ) : null}
72
- </div>
73
- </CardHeader>
74
- </Card>
52
+ <div className="flex flex-wrap items-start justify-between gap-4">
53
+ <div>
54
+ <h2 className="font-semibold text-lg">{labels.accessLevelsLabel}</h2>
55
+ <p className="text-muted-foreground text-sm">
56
+ {t('ws-roles.description')}
57
+ </p>
58
+ </div>
59
+ {canManageRoles ? (
60
+ <Button onClick={onCreateRole}>
61
+ <Plus className="mr-2 h-4 w-4" />
62
+ {t('ws-roles.create')}
63
+ </Button>
64
+ ) : null}
65
+ </div>
75
66
 
76
67
  {isLoading ? (
77
68
  <div className="space-y-3">
78
- <Skeleton className="h-36 rounded-lg" />
79
- <Skeleton className="h-36 rounded-lg" />
69
+ <Skeleton className="h-36 rounded-xl" />
70
+ <Skeleton className="h-36 rounded-xl" />
80
71
  </div>
81
72
  ) : roles.length === 0 ? (
82
- <Card className="shadow-none">
83
- <CardContent className="flex min-h-44 flex-col items-center justify-center gap-2 text-center">
84
- <div className="font-medium">{labels.rolesEmptyTitle}</div>
85
- <div className="text-muted-foreground text-sm">
86
- {labels.rolesEmptyDescription}
87
- </div>
88
- </CardContent>
89
- </Card>
73
+ <div className="flex min-h-44 flex-col items-center justify-center gap-2 rounded-xl border border-border border-dashed p-8 text-center">
74
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-dynamic-purple/10 text-dynamic-purple">
75
+ <ShieldCheck className="h-6 w-6" />
76
+ </div>
77
+ <div className="font-medium">{labels.rolesEmptyTitle}</div>
78
+ <div className="max-w-sm text-muted-foreground text-sm">
79
+ {labels.rolesEmptyDescription}
80
+ </div>
81
+ </div>
90
82
  ) : (
91
83
  <div className="grid gap-3">
92
84
  {roles.map((role) => {
@@ -94,74 +86,104 @@ export function WorkspaceAccessRoles({
94
86
  role.members && role.members.length > 0
95
87
  ? role.members
96
88
  : assignedMembersForRole(role.id, members);
89
+ const enabled = enabledPermissionCount(role);
90
+ const pct =
91
+ permissionCount > 0
92
+ ? Math.round((enabled / permissionCount) * 100)
93
+ : 0;
97
94
 
98
95
  return (
99
- <Card key={role.id} className="shadow-none">
100
- <CardContent className="space-y-4 p-5">
101
- <div className="flex flex-wrap items-start justify-between gap-4">
102
- <div className="space-y-2">
103
- <div className="font-medium text-lg">{role.name}</div>
104
- <div className="flex flex-wrap gap-2 text-muted-foreground text-sm">
105
- <span>
106
- {t('ws-roles.members')}: {assignedMembers.length}
107
- </span>
108
- <span>
109
- {t('ws-roles.permissions')}:{' '}
110
- {enabledPermissionCount(role)}/{permissionCount}
111
- </span>
112
- </div>
96
+ <div
97
+ key={role.id}
98
+ className="rounded-xl border border-border bg-background p-5 transition-colors hover:border-foreground/20"
99
+ >
100
+ <div className="flex flex-wrap items-start justify-between gap-4">
101
+ <div className="flex min-w-0 gap-3">
102
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-dynamic-purple/30 bg-dynamic-purple/10 text-dynamic-purple">
103
+ <ShieldCheck className="h-5 w-5" />
113
104
  </div>
114
-
115
- {canManageRoles ? (
116
- <div className="flex flex-wrap gap-2">
117
- <Button
118
- variant="outline"
119
- onClick={() => onEditRole(role)}
120
- >
121
- <Pencil className="mr-2 h-4 w-4" />
122
- {t('common.edit')}
123
- </Button>
124
- <Button
105
+ <div className="min-w-0">
106
+ <div className="flex items-center gap-2">
107
+ <span className="truncate font-semibold text-base">
108
+ {role.name}
109
+ </span>
110
+ <Badge
125
111
  variant="outline"
126
- onClick={() => onDeleteRole(role)}
112
+ className="h-5 gap-1 px-1.5 text-xs"
127
113
  >
128
- <Trash2 className="mr-2 h-4 w-4" />
129
- {t('common.delete')}
130
- </Button>
114
+ <Users className="h-3 w-3" />
115
+ {assignedMembers.length}
116
+ </Badge>
131
117
  </div>
132
- ) : null}
118
+ <div className="mt-2 max-w-xs">
119
+ <div className="flex items-center justify-between text-muted-foreground text-xs">
120
+ <span>{t('ws-roles.permissions')}</span>
121
+ <span className="tabular-nums">
122
+ {enabled}/{permissionCount}
123
+ </span>
124
+ </div>
125
+ <div className="mt-1 h-1.5 overflow-hidden rounded-full bg-foreground/10">
126
+ <div
127
+ className="h-full rounded-full bg-dynamic-purple"
128
+ style={{ width: `${pct}%` }}
129
+ />
130
+ </div>
131
+ </div>
132
+ </div>
133
133
  </div>
134
134
 
135
- <div className="flex flex-wrap gap-2">
136
- <WorkspaceAccessPermissionPreview
137
- emptyLabel={t('ws-members.no_permissions')}
138
- permissionTitles={permissionTitles}
139
- role={role}
140
- />
141
- </div>
135
+ {canManageRoles ? (
136
+ <div className="flex flex-wrap gap-2">
137
+ <Button
138
+ variant="outline"
139
+ size="sm"
140
+ onClick={() => onEditRole(role)}
141
+ >
142
+ <Pencil className="mr-2 h-4 w-4" />
143
+ {t('common.edit')}
144
+ </Button>
145
+ <Button
146
+ variant="outline"
147
+ size="sm"
148
+ onClick={() => onDeleteRole(role)}
149
+ >
150
+ <Trash2 className="mr-2 h-4 w-4" />
151
+ {t('common.delete')}
152
+ </Button>
153
+ </div>
154
+ ) : null}
155
+ </div>
156
+
157
+ <div className="mt-4 flex flex-wrap gap-2">
158
+ <WorkspaceAccessPermissionPreview
159
+ emptyLabel={t('ws-members.no_permissions')}
160
+ permissionTitles={permissionTitles}
161
+ role={role}
162
+ />
163
+ </div>
142
164
 
143
- <div className="flex flex-wrap gap-2">
144
- {assignedMembers.length === 0 ? (
145
- <Badge variant="outline" className="rounded-full">
146
- {labels.noRolesLabel}
165
+ {assignedMembers.length > 0 ? (
166
+ <div className="mt-3 flex flex-wrap gap-1.5 border-border border-t pt-3">
167
+ {assignedMembers.slice(0, 6).map((member) => (
168
+ <Badge
169
+ key={`${role.id}-${member.id}`}
170
+ variant="secondary"
171
+ className="rounded-full text-xs"
172
+ >
173
+ {getMemberDisplayName(
174
+ member as InternalApiEnhancedWorkspaceMember,
175
+ t('common.unknown')
176
+ )}
147
177
  </Badge>
148
- ) : (
149
- assignedMembers.slice(0, 6).map((member) => (
150
- <Badge
151
- key={`${role.id}-${member.id}`}
152
- variant="outline"
153
- className="rounded-full"
154
- >
155
- {getMemberDisplayName(
156
- member as InternalApiEnhancedWorkspaceMember,
157
- t('common.unknown')
158
- )}
159
- </Badge>
160
- ))
161
- )}
178
+ ))}
179
+ {assignedMembers.length > 6 ? (
180
+ <Badge variant="outline" className="rounded-full text-xs">
181
+ +{assignedMembers.length - 6}
182
+ </Badge>
183
+ ) : null}
162
184
  </div>
163
- </CardContent>
164
- </Card>
185
+ ) : null}
186
+ </div>
165
187
  );
166
188
  })}
167
189
  </div>
@@ -1,10 +1,18 @@
1
1
  'use client';
2
2
 
3
- import { Search, UserPlus } from '@tuturuuu/icons';
3
+ import {
4
+ KeyRound,
5
+ Search,
6
+ ShieldCheck,
7
+ ShieldUser,
8
+ UserPlus,
9
+ Users,
10
+ } from '@tuturuuu/icons';
4
11
  import { Button } from '@tuturuuu/ui/button';
5
12
  import { Input } from '@tuturuuu/ui/input';
6
13
  import { TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
7
14
  import { useTranslations } from 'next-intl';
15
+ import type { ReactNode } from 'react';
8
16
  import type { WorkspaceAccessTab } from './types';
9
17
 
10
18
  type Props = {
@@ -18,6 +26,9 @@ type Props = {
18
26
  search: string;
19
27
  };
20
28
 
29
+ const TAB_TRIGGER_CLASS =
30
+ 'rounded-none border-transparent border-b-2 bg-transparent px-1 pt-1 pb-3 text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:border-dynamic-blue data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none';
31
+
21
32
  export function WorkspaceAccessTabsToolbar({
22
33
  activeTab,
23
34
  accessLevelsLabel,
@@ -30,41 +41,71 @@ export function WorkspaceAccessTabsToolbar({
30
41
  }: Props) {
31
42
  const t = useTranslations() as (key: string) => string;
32
43
 
44
+ const tabs: Array<{
45
+ disabled?: boolean;
46
+ icon: ReactNode;
47
+ label: string;
48
+ value: WorkspaceAccessTab;
49
+ }> = [
50
+ {
51
+ icon: <Users className="h-4 w-4" />,
52
+ label: t('ws-roles.members'),
53
+ value: 'people',
54
+ },
55
+ {
56
+ disabled: !canManageRoles,
57
+ icon: <ShieldCheck className="h-4 w-4" />,
58
+ label: accessLevelsLabel,
59
+ value: 'roles',
60
+ },
61
+ {
62
+ disabled: !canManageRoles,
63
+ icon: <ShieldUser className="h-4 w-4" />,
64
+ label: t('ws-roles.member_defaults_tab'),
65
+ value: 'defaults-member',
66
+ },
67
+ {
68
+ disabled: !canManageRoles,
69
+ icon: <KeyRound className="h-4 w-4" />,
70
+ label: t('ws-roles.guest_defaults_tab'),
71
+ value: 'defaults-guest',
72
+ },
73
+ ];
74
+
33
75
  return (
34
- <div className="rounded-xl border border-border bg-background p-3 shadow-sm">
35
- <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
36
- <TabsList className="grid h-auto w-full grid-cols-2 text-sm lg:w-auto lg:grid-cols-4">
37
- <TabsTrigger value="people">{t('ws-roles.members')}</TabsTrigger>
38
- <TabsTrigger value="roles" disabled={!canManageRoles}>
39
- {accessLevelsLabel}
40
- </TabsTrigger>
41
- <TabsTrigger value="defaults-member" disabled={!canManageRoles}>
42
- {t('ws-roles.member_defaults_tab')}
43
- </TabsTrigger>
44
- <TabsTrigger value="defaults-guest" disabled={!canManageRoles}>
45
- {t('ws-roles.guest_defaults_tab')}
76
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
77
+ <TabsList className="h-auto w-full justify-start gap-5 overflow-x-auto rounded-none border-border border-b bg-transparent p-0 lg:w-auto">
78
+ {tabs.map((tab) => (
79
+ <TabsTrigger
80
+ key={tab.value}
81
+ value={tab.value}
82
+ disabled={tab.disabled}
83
+ className={TAB_TRIGGER_CLASS}
84
+ >
85
+ {tab.icon}
86
+ {tab.label}
46
87
  </TabsTrigger>
47
- </TabsList>
88
+ ))}
89
+ </TabsList>
48
90
 
49
- <div className="flex w-full flex-col gap-3 sm:flex-row lg:w-auto">
50
- <div className="relative min-w-0 sm:min-w-[320px]">
51
- <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
52
- <Input
53
- value={search}
54
- onChange={(event) => onSearchChange(event.target.value)}
55
- placeholder={t('common.search')}
56
- className="pl-9"
57
- />
58
- </div>
59
- {activeTab === 'people' ? (
60
- <Button disabled={!canInvite} onClick={onInviteClick}>
61
- <UserPlus className="mr-2 h-4 w-4" />
62
- {disableInvite
63
- ? t('ws-members.invite_member_disabled')
64
- : t('ws-members.invite_member')}
65
- </Button>
66
- ) : null}
91
+ <div className="flex w-full shrink-0 flex-col gap-2 sm:flex-row lg:w-auto">
92
+ <div className="relative min-w-0 sm:min-w-[280px]">
93
+ <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
94
+ <Input
95
+ value={search}
96
+ onChange={(event) => onSearchChange(event.target.value)}
97
+ placeholder={t('common.search')}
98
+ className="pl-9"
99
+ />
67
100
  </div>
101
+ {activeTab === 'people' ? (
102
+ <Button disabled={!canInvite} onClick={onInviteClick}>
103
+ <UserPlus className="mr-2 h-4 w-4" />
104
+ {disableInvite
105
+ ? t('ws-members.invite_member_disabled')
106
+ : t('ws-members.invite_member')}
107
+ </Button>
108
+ ) : null}
68
109
  </div>
69
110
  </div>
70
111
  );