@almanaclabs/blaze-ui 0.4.0 → 0.6.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 (122) hide show
  1. package/dist/components/accordion/accordion.js +2 -2
  2. package/dist/components/alert/alert.d.ts +1 -1
  3. package/dist/components/alert/alert.js +4 -4
  4. package/dist/components/alert-dialog/alert-dialog.d.ts.map +1 -1
  5. package/dist/components/alert-dialog/alert-dialog.js +3 -3
  6. package/dist/components/attachment/attachment.d.ts +2 -2
  7. package/dist/components/attachment/attachment.js +3 -3
  8. package/dist/components/avatar/avatar.d.ts +6 -4
  9. package/dist/components/avatar/avatar.d.ts.map +1 -1
  10. package/dist/components/avatar/avatar.js +10 -9
  11. package/dist/components/badge/badge.d.ts +3 -2
  12. package/dist/components/badge/badge.d.ts.map +1 -1
  13. package/dist/components/badge/badge.js +30 -14
  14. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  15. package/dist/components/breadcrumb/breadcrumb.js +2 -2
  16. package/dist/components/bubble/bubble.d.ts +1 -1
  17. package/dist/components/bubble/bubble.js +1 -1
  18. package/dist/components/button/button.d.ts +5 -3
  19. package/dist/components/button/button.d.ts.map +1 -1
  20. package/dist/components/button/button.js +46 -27
  21. package/dist/components/button/index.d.ts +1 -1
  22. package/dist/components/button/index.d.ts.map +1 -1
  23. package/dist/components/button-group/button-group.js +4 -4
  24. package/dist/components/calendar/calendar.d.ts.map +1 -1
  25. package/dist/components/calendar/calendar.js +18 -18
  26. package/dist/components/card/card.js +4 -4
  27. package/dist/components/carousel/carousel.d.ts.map +1 -1
  28. package/dist/components/carousel/carousel.js +31 -31
  29. package/dist/components/chart/chart.js +34 -34
  30. package/dist/components/checkbox/checkbox.js +1 -1
  31. package/dist/components/combobox/combobox.d.ts.map +1 -1
  32. package/dist/components/combobox/combobox.js +44 -44
  33. package/dist/components/data-table/data-table.js +1 -1
  34. package/dist/components/date-picker/date-picker.js +10 -10
  35. package/dist/components/dialog/dialog.js +3 -3
  36. package/dist/components/drawer/drawer.js +37 -37
  37. package/dist/components/dropdown-menu/dropdown-menu.js +7 -7
  38. package/dist/components/empty/empty.d.ts.map +1 -1
  39. package/dist/components/empty/empty.js +4 -4
  40. package/dist/components/field/field.d.ts.map +1 -1
  41. package/dist/components/field/field.js +28 -28
  42. package/dist/components/identity-mark/identity-mark.d.ts +5 -0
  43. package/dist/components/identity-mark/identity-mark.d.ts.map +1 -0
  44. package/dist/components/identity-mark/identity-mark.js +20 -0
  45. package/dist/components/identity-mark/index.d.ts +2 -0
  46. package/dist/components/identity-mark/index.d.ts.map +1 -0
  47. package/dist/components/input/input.d.ts +9 -2
  48. package/dist/components/input/input.d.ts.map +1 -1
  49. package/dist/components/input/input.js +23 -10
  50. package/dist/components/input-group/input-group.d.ts +11 -3
  51. package/dist/components/input-group/input-group.d.ts.map +1 -1
  52. package/dist/components/input-group/input-group.js +56 -38
  53. package/dist/components/item/item.d.ts +2 -2
  54. package/dist/components/item/item.js +2 -2
  55. package/dist/components/label/label.js +2 -2
  56. package/dist/components/marker/marker.d.ts +1 -1
  57. package/dist/components/marker/marker.js +3 -3
  58. package/dist/components/message/message.js +1 -1
  59. package/dist/components/metric-overview/index.d.ts +2 -0
  60. package/dist/components/metric-overview/index.d.ts.map +1 -0
  61. package/dist/components/metric-overview/metric-overview.d.ts +12 -0
  62. package/dist/components/metric-overview/metric-overview.d.ts.map +1 -0
  63. package/dist/components/metric-overview/metric-overview.js +42 -0
  64. package/dist/components/native-select/native-select.d.ts +2 -1
  65. package/dist/components/native-select/native-select.d.ts.map +1 -1
  66. package/dist/components/native-select/native-select.js +22 -21
  67. package/dist/components/navigation-action/index.d.ts +2 -0
  68. package/dist/components/navigation-action/index.d.ts.map +1 -0
  69. package/dist/components/navigation-action/navigation-action.d.ts +6 -0
  70. package/dist/components/navigation-action/navigation-action.d.ts.map +1 -0
  71. package/dist/components/navigation-action/navigation-action.js +18 -0
  72. package/dist/components/pagination/pagination.d.ts.map +1 -1
  73. package/dist/components/pagination/pagination.js +6 -6
  74. package/dist/components/popover/popover.js +1 -1
  75. package/dist/components/progress/progress.js +2 -2
  76. package/dist/components/questionnaire/questionnaire-primitive.js +71 -71
  77. package/dist/components/questionnaire/questionnaire.d.ts.map +1 -1
  78. package/dist/components/questionnaire/questionnaire.js +7 -7
  79. package/dist/components/scroll-area/scroll-area.js +1 -1
  80. package/dist/components/select/select.d.ts +2 -1
  81. package/dist/components/select/select.d.ts.map +1 -1
  82. package/dist/components/select/select.js +50 -49
  83. package/dist/components/sheet/sheet.js +2 -2
  84. package/dist/components/sidebar/sidebar.d.ts +13 -2
  85. package/dist/components/sidebar/sidebar.d.ts.map +1 -1
  86. package/dist/components/sidebar/sidebar.js +156 -95
  87. package/dist/components/spinner/spinner.js +1 -1
  88. package/dist/components/table/table.d.ts +4 -1
  89. package/dist/components/table/table.d.ts.map +1 -1
  90. package/dist/components/table/table.js +11 -9
  91. package/dist/components/tabs/tabs.d.ts +1 -1
  92. package/dist/components/tabs/tabs.js +2 -2
  93. package/dist/components/textarea/textarea.js +2 -2
  94. package/dist/components/toast/toast.d.ts.map +1 -1
  95. package/dist/components/toast/toast.js +6 -6
  96. package/dist/components/toggle/toggle.d.ts +1 -1
  97. package/dist/components/toggle/toggle.js +1 -1
  98. package/dist/components/toggle-group/toggle-group.js +10 -10
  99. package/dist/components/tooltip/tooltip.js +1 -1
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +58 -55
  103. package/dist/lib/control-size.d.ts +9 -0
  104. package/dist/lib/control-size.d.ts.map +1 -0
  105. package/dist/lib/control-size.js +10 -0
  106. package/dist/recipes/recipe-runtime.d.ts +12 -0
  107. package/dist/recipes/recipe-runtime.d.ts.map +1 -1
  108. package/package.json +1 -1
  109. package/recipes/README.md +2 -0
  110. package/recipes/guided-workflow/README.md +51 -0
  111. package/recipes/guided-workflow/guided-workflow.tsx +495 -0
  112. package/recipes/manifest.json +8 -1
  113. package/recipes/workspace-shell/README.md +79 -25
  114. package/recipes/workspace-shell/assets/google-search-ads.svg +6 -0
  115. package/recipes/workspace-shell/assets/home.svg +5 -0
  116. package/recipes/workspace-shell/assets/radiant-health.png +0 -0
  117. package/recipes/workspace-shell/workspace-shell.css +55 -0
  118. package/recipes/workspace-shell/workspace-shell.tsx +463 -220
  119. package/src/styles/fonts/Sohne-Book.woff2 +0 -0
  120. package/src/styles/fonts/Sohne-Medium.woff2 +0 -0
  121. package/src/styles/fonts/Sohne-Semi.woff2 +0 -0
  122. package/src/styles/globals.css +235 -56
@@ -1,19 +1,24 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
1
4
  import type { ComponentProps, ComponentType, ReactNode } from 'react'
2
5
  import {
3
- IconArrowsUpDown,
4
- IconBolt,
5
- IconBrowser,
6
- IconChartBar,
7
- IconHome,
6
+ IconAd2,
7
+ IconChevronDown,
8
+ IconFileAnalytics,
9
+ IconIdBadge,
10
+ IconLayoutSidebarLeftCollapse,
11
+ IconLayoutSidebarLeftExpand,
8
12
  IconLogout,
9
- IconSearch,
13
+ IconMessageStar,
14
+ IconMessages,
15
+ IconPlugConnected,
16
+ IconScoreboard,
10
17
  IconSettings,
11
- IconStar,
12
- IconTags,
13
- IconTarget,
14
18
  IconUser,
15
19
  IconUsers,
16
- IconWorld,
20
+ IconWorldSearch,
21
+ IconWorldWww,
17
22
  } from '@tabler/icons-react'
18
23
 
19
24
  import {
@@ -27,27 +32,66 @@ import {
27
32
  DropdownMenuLabel,
28
33
  DropdownMenuSeparator,
29
34
  DropdownMenuTrigger,
35
+ IdentityMark,
36
+ IdentityMarkImage,
30
37
  Sidebar,
31
38
  SidebarContent,
32
39
  SidebarFooter,
33
40
  SidebarGroup,
34
41
  SidebarGroupContent,
35
42
  SidebarGroupLabel,
36
- SidebarHeader,
37
43
  SidebarInset,
38
44
  SidebarMenu,
39
45
  SidebarMenuButton,
40
46
  SidebarMenuItem,
41
47
  SidebarProvider,
42
48
  SidebarRail,
43
- SidebarTrigger,
44
49
  TooltipProvider,
50
+ cn,
51
+ useSidebar,
45
52
  } from '@almanaclabs/blaze-ui'
46
53
 
47
- type NavigationIcon = ComponentType<{
54
+ import './workspace-shell.css'
55
+
56
+ const googleSearchAdsLogo = new URL(
57
+ './assets/google-search-ads.svg',
58
+ import.meta.url,
59
+ ).href
60
+ const radiantHealthLogo = new URL(
61
+ './assets/radiant-health.png',
62
+ import.meta.url,
63
+ ).href
64
+
65
+ type NavigationIconProps = {
48
66
  className?: string
49
67
  'aria-hidden'?: boolean | 'true' | 'false'
50
- }>
68
+ }
69
+
70
+ type NavigationIcon = ComponentType<NavigationIconProps>
71
+
72
+ function HomeNavigationIcon({
73
+ className,
74
+ 'aria-hidden': ariaHidden,
75
+ }: NavigationIconProps) {
76
+ return (
77
+ <svg
78
+ viewBox="0 0 15 15"
79
+ fill="none"
80
+ aria-hidden={ariaHidden}
81
+ focusable="false"
82
+ className={cn('shrink-0', className)}
83
+ >
84
+ <path
85
+ d="M4.6875 10.6641H10.3125M7.06536 2.00698L2.19036 5.30377C1.99251 5.43757 1.875 5.65416 1.875 5.88503V12.0551C1.875 12.646 2.37868 13.125 3 13.125H12C12.6213 13.125 13.125 12.646 13.125 12.0551V5.88503C13.125 5.65416 13.0075 5.43757 12.8096 5.30377L7.93464 2.00698C7.67442 1.83101 7.32558 1.83101 7.06536 2.00698Z"
86
+ stroke="currentColor"
87
+ strokeLinecap="round"
88
+ strokeLinejoin="round"
89
+ vectorEffect="non-scaling-stroke"
90
+ style={{ strokeWidth: 'var(--icon-stroke-15)' }}
91
+ />
92
+ </svg>
93
+ )
94
+ }
51
95
 
52
96
  export type WorkspaceShellNavigationItem = {
53
97
  key: string
@@ -56,67 +100,113 @@ export type WorkspaceShellNavigationItem = {
56
100
  icon: NavigationIcon
57
101
  }
58
102
 
59
- export type WorkspaceShellSubNavigationItem = {
103
+ export type WorkspaceShellNavigationGroup = {
60
104
  key: string
61
105
  label: string
62
- href: string
106
+ items: readonly WorkspaceShellNavigationItem[]
63
107
  }
64
108
 
65
109
  export type WorkspaceShellRecipeProps = {
66
- title: string
67
- workspaceName: string
68
- userEmail: string
69
110
  activeNavigationKey: string
70
- activeSubNavigationKey?: string
71
- accessMode?: 'am' | 'client'
111
+ channelTitle: string
72
112
  children?: ReactNode
113
+ channelMark?: ReactNode
73
114
  defaultSidebarOpen?: boolean
74
- subNavigation?: readonly WorkspaceShellSubNavigationItem[]
115
+ defaultSidebarWidth?: number
116
+ footer?: ReactNode
117
+ headerAction?: ReactNode
118
+ mode?: 'standard' | 'focused'
119
+ maxSidebarWidth?: number
120
+ minSidebarWidth?: number
121
+ navigationGroups?: readonly WorkspaceShellNavigationGroup[]
122
+ pageHeader?: ReactNode
123
+ onSidebarWidthChange?: (width: number) => void
124
+ userAvatar?: ReactNode
125
+ userName: string
126
+ workspaceMark?: ReactNode
127
+ workspaceName: string
75
128
  }
76
129
 
77
- const primaryNavigation: readonly WorkspaceShellNavigationItem[] = [
78
- { key: 'home', label: 'Home', href: '#home', icon: IconHome },
79
- {
80
- key: 'analytics',
81
- label: 'Analytics',
82
- href: '#analytics',
83
- icon: IconChartBar,
84
- },
85
- {
86
- key: 'integrations',
87
- label: 'Integrations',
88
- href: '#integrations',
89
- icon: IconBolt,
90
- },
91
- { key: 'brand-kit', label: 'Brand Kit', href: '#brand-kit', icon: IconTags },
92
- {
93
- key: 'scorecard',
94
- label: 'Scorecard',
95
- href: '#scorecard',
96
- icon: IconTarget,
97
- },
98
- ]
130
+ const DEFAULT_SIDEBAR_WIDTH = 220
131
+ const MIN_SIDEBAR_WIDTH = 200
132
+ const MAX_SIDEBAR_WIDTH = 320
99
133
 
100
- const channelNavigation: readonly WorkspaceShellNavigationItem[] = [
101
- {
102
- key: 'google-ads',
103
- label: 'Google Ads',
104
- href: '#google-ads',
105
- icon: IconSearch,
106
- },
107
- { key: 'seo-aeo', label: 'SEO/AEO', href: '#seo-aeo', icon: IconWorld },
108
- { key: 'website', label: 'Website', href: '#website', icon: IconBrowser },
134
+ function clampSidebarWidth(width: number, min: number, max: number) {
135
+ return Math.min(Math.max(width, min), max)
136
+ }
137
+
138
+ const defaultNavigationGroups: readonly WorkspaceShellNavigationGroup[] = [
109
139
  {
110
- key: 'reputation',
111
- label: 'Reputation',
112
- href: '#reputation',
113
- icon: IconStar,
140
+ key: 'primary',
141
+ label: 'Manage',
142
+ items: [
143
+ {
144
+ key: 'home',
145
+ label: 'Home',
146
+ href: '#home',
147
+ icon: HomeNavigationIcon,
148
+ },
149
+ {
150
+ key: 'analytics',
151
+ label: 'Analytics',
152
+ href: '#analytics',
153
+ icon: IconFileAnalytics,
154
+ },
155
+ {
156
+ key: 'integrations',
157
+ label: 'Integrations',
158
+ href: '#integrations',
159
+ icon: IconPlugConnected,
160
+ },
161
+ {
162
+ key: 'brand-kit',
163
+ label: 'Brand Kit',
164
+ href: '#brand-kit',
165
+ icon: IconIdBadge,
166
+ },
167
+ {
168
+ key: 'scorecard',
169
+ label: 'Scorecard',
170
+ href: '#scorecard',
171
+ icon: IconScoreboard,
172
+ },
173
+ ],
114
174
  },
115
175
  {
116
- key: 'inbound-sdr',
117
- label: 'Inbound SDR',
118
- href: '#inbound-sdr',
119
- icon: IconUsers,
176
+ key: 'channels',
177
+ label: 'Channels',
178
+ items: [
179
+ {
180
+ key: 'google-ads',
181
+ label: 'Google Ads',
182
+ href: '#google-ads',
183
+ icon: IconAd2,
184
+ },
185
+ {
186
+ key: 'seo-aeo',
187
+ label: 'SEO/AEO',
188
+ href: '#seo-aeo',
189
+ icon: IconWorldSearch,
190
+ },
191
+ {
192
+ key: 'website',
193
+ label: 'Website',
194
+ href: '#website',
195
+ icon: IconWorldWww,
196
+ },
197
+ {
198
+ key: 'reputation',
199
+ label: 'Reputation',
200
+ href: '#reputation',
201
+ icon: IconMessageStar,
202
+ },
203
+ {
204
+ key: 'inbound-sdr',
205
+ label: 'Inbound SDR',
206
+ href: '#inbound-sdr',
207
+ icon: IconMessages,
208
+ },
209
+ ],
120
210
  },
121
211
  ]
122
212
 
@@ -141,66 +231,139 @@ function RecipeLink({
141
231
  )
142
232
  }
143
233
 
144
- function WorkspaceSelector({ workspaceName }: { workspaceName: string }) {
234
+ function DefaultWorkspaceMark() {
145
235
  return (
146
- <SidebarMenu>
147
- <SidebarMenuItem>
148
- <DropdownMenu>
149
- <DropdownMenuTrigger
150
- render={
151
- <SidebarMenuButton
152
- size="lg"
153
- aria-label={`Open workspace menu for ${workspaceName}`}
154
- className="data-[popup-open]:bg-sidebar-accent data-[popup-open]:text-sidebar-accent-foreground"
155
- />
156
- }
157
- >
158
- <span className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary font-semibold text-sidebar-primary-foreground">
159
- {initials(workspaceName).slice(0, 1)}
160
- </span>
161
- <span className="grid flex-1 text-left text-sm leading-tight">
162
- <span className="truncate font-medium">{workspaceName}</span>
163
- <span className="truncate text-xs text-sidebar-foreground/65">
164
- Client workspace
165
- </span>
166
- </span>
167
- <IconArrowsUpDown className="ml-auto" aria-hidden="true" />
168
- </DropdownMenuTrigger>
169
- <DropdownMenuContent align="start" side="right" className="min-w-56">
170
- <DropdownMenuGroup>
171
- <DropdownMenuLabel>Workspace</DropdownMenuLabel>
172
- <DropdownMenuItem>
173
- <IconUsers aria-hidden="true" />
174
- All workspaces
175
- </DropdownMenuItem>
176
- </DropdownMenuGroup>
177
- <DropdownMenuSeparator />
178
- <DropdownMenuItem>
179
- <IconLogout aria-hidden="true" />
180
- Sign out
181
- </DropdownMenuItem>
182
- </DropdownMenuContent>
183
- </DropdownMenu>
184
- </SidebarMenuItem>
185
- </SidebarMenu>
236
+ <IdentityMark data-identity="workspace" className="bg-[#45164a]">
237
+ <IdentityMarkImage
238
+ data-identity-artwork="workspace"
239
+ src={radiantHealthLogo}
240
+ alt=""
241
+ aria-hidden="true"
242
+ className="rounded-[4px] object-cover"
243
+ />
244
+ </IdentityMark>
245
+ )
246
+ }
247
+
248
+ function DefaultChannelMark() {
249
+ return (
250
+ <IdentityMark data-identity="channel">
251
+ <IdentityMarkImage
252
+ data-identity-artwork="channel"
253
+ src={googleSearchAdsLogo}
254
+ alt=""
255
+ aria-hidden="true"
256
+ />
257
+ </IdentityMark>
258
+ )
259
+ }
260
+
261
+ function WorkspaceSidebarTrigger() {
262
+ const { state, toggleSidebar } = useSidebar()
263
+
264
+ if (state === 'collapsed') {
265
+ return null
266
+ }
267
+
268
+ return (
269
+ <Button
270
+ type="button"
271
+ variant="tertiary"
272
+ size="icon-sm"
273
+ aria-label="Collapse workspace navigation"
274
+ onClick={toggleSidebar}
275
+ className="mr-px shrink-0 text-foreground group-data-[collapsible=icon]:hidden"
276
+ >
277
+ <IconLayoutSidebarLeftCollapse aria-hidden="true" />
278
+ </Button>
279
+ )
280
+ }
281
+
282
+ function WorkspaceFrameSidebarTrigger() {
283
+ const { isMobile, state, toggleSidebar } = useSidebar()
284
+
285
+ if (!isMobile && state !== 'collapsed') {
286
+ return null
287
+ }
288
+
289
+ return (
290
+ <Button
291
+ type="button"
292
+ variant="tertiary"
293
+ size="icon-sm"
294
+ aria-label={
295
+ isMobile ? 'Open workspace navigation' : 'Expand workspace navigation'
296
+ }
297
+ onClick={toggleSidebar}
298
+ className="shrink-0 text-foreground"
299
+ >
300
+ <IconLayoutSidebarLeftExpand aria-hidden="true" />
301
+ </Button>
302
+ )
303
+ }
304
+
305
+ function WorkspaceMenu({
306
+ workspaceMark,
307
+ workspaceName,
308
+ }: {
309
+ workspaceMark: ReactNode
310
+ workspaceName: string
311
+ }) {
312
+ return (
313
+ <DropdownMenu>
314
+ <DropdownMenuTrigger
315
+ render={
316
+ <Button
317
+ type="button"
318
+ variant="tertiary"
319
+ size="md"
320
+ aria-label={`Open workspace menu for ${workspaceName}`}
321
+ className="min-w-0 flex-1 justify-start gap-1.5 px-[10px] text-left group-data-[collapsible=icon]:size-6 group-data-[collapsible=icon]:flex-none group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0"
322
+ />
323
+ }
324
+ >
325
+ {workspaceMark}
326
+ <span className="truncate text-sm font-medium leading-[1.4] tracking-[0.01em] group-data-[collapsible=icon]:hidden">
327
+ {workspaceName}
328
+ </span>
329
+ <IconChevronDown
330
+ className="icon-optical-16 shrink-0 group-data-[collapsible=icon]:hidden"
331
+ aria-hidden="true"
332
+ />
333
+ </DropdownMenuTrigger>
334
+ <DropdownMenuContent align="start" side="right" className="min-w-56">
335
+ <DropdownMenuGroup>
336
+ <DropdownMenuLabel>Workspace</DropdownMenuLabel>
337
+ <DropdownMenuItem>
338
+ <IconUsers aria-hidden="true" />
339
+ All workspaces
340
+ </DropdownMenuItem>
341
+ </DropdownMenuGroup>
342
+ <DropdownMenuSeparator />
343
+ <DropdownMenuItem>
344
+ <IconLogout aria-hidden="true" />
345
+ Sign out
346
+ </DropdownMenuItem>
347
+ </DropdownMenuContent>
348
+ </DropdownMenu>
186
349
  )
187
350
  }
188
351
 
189
352
  function NavigationGroup({
190
- items,
353
+ group,
191
354
  activeKey,
192
- label,
193
355
  }: {
194
- items: readonly WorkspaceShellNavigationItem[]
356
+ group: WorkspaceShellNavigationGroup
195
357
  activeKey: string
196
- label?: string
197
358
  }) {
198
359
  return (
199
- <SidebarGroup>
200
- {label && <SidebarGroupLabel>{label}</SidebarGroupLabel>}
360
+ <SidebarGroup className="gap-0.5 p-0">
361
+ <SidebarGroupLabel className="h-[27px] rounded-[2px] px-[10px] text-xs font-medium tracking-[0.02em] text-foreground group-data-[collapsible=icon]:mt-0! group-data-[collapsible=icon]:opacity-0">
362
+ {group.label}
363
+ </SidebarGroupLabel>
201
364
  <SidebarGroupContent>
202
- <SidebarMenu>
203
- {items.map((item) => (
365
+ <SidebarMenu className="gap-px">
366
+ {group.items.map((item) => (
204
367
  <SidebarMenuItem key={item.key}>
205
368
  <SidebarMenuButton
206
369
  tooltip={item.label}
@@ -208,12 +371,18 @@ function NavigationGroup({
208
371
  render={
209
372
  <RecipeLink
210
373
  href={item.href}
374
+ aria-label={item.label}
211
375
  aria-current={item.key === activeKey ? 'page' : undefined}
212
376
  />
213
377
  }
214
378
  >
215
- <item.icon aria-hidden="true" />
216
- <span>{item.label}</span>
379
+ <item.icon
380
+ className="icon-optical-15 text-foreground"
381
+ aria-hidden="true"
382
+ />
383
+ <span className="workspace-navigation-label group-data-[collapsible=icon]:translate-x-1 group-data-[collapsible=icon]:opacity-0">
384
+ {item.label}
385
+ </span>
217
386
  </SidebarMenuButton>
218
387
  </SidebarMenuItem>
219
388
  ))}
@@ -223,29 +392,34 @@ function NavigationGroup({
223
392
  )
224
393
  }
225
394
 
226
- function AccountMenu({ userEmail }: { userEmail: string }) {
395
+ function AccountMenu({
396
+ userAvatar,
397
+ userName,
398
+ }: {
399
+ userAvatar: ReactNode
400
+ userName: string
401
+ }) {
227
402
  return (
228
403
  <DropdownMenu>
229
404
  <DropdownMenuTrigger
230
405
  render={
231
406
  <Button
232
- variant="ghost"
233
- size="icon"
234
- aria-label={`Open account menu. Signed in as ${userEmail}`}
235
- className="rounded-full"
407
+ type="button"
408
+ variant="tertiary"
409
+ size="md"
410
+ aria-label={`Open account menu for ${userName}`}
411
+ className="min-w-0 flex-1 justify-start gap-1.5 px-[10px] text-left font-normal group-data-[collapsible=icon]:size-5 group-data-[collapsible=icon]:flex-none group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0"
236
412
  />
237
413
  }
238
414
  >
239
- <Avatar size="sm">
240
- <AvatarFallback>{initials(userEmail)}</AvatarFallback>
241
- </Avatar>
415
+ {userAvatar}
416
+ <span className="truncate text-sm font-normal leading-[1.4] tracking-[0.01em] group-data-[collapsible=icon]:hidden">
417
+ {userName}
418
+ </span>
242
419
  </DropdownMenuTrigger>
243
- <DropdownMenuContent align="end" className="min-w-56">
244
- <DropdownMenuGroup>
245
- <DropdownMenuLabel>{userEmail}</DropdownMenuLabel>
246
- </DropdownMenuGroup>
247
- <DropdownMenuSeparator />
420
+ <DropdownMenuContent align="start" side="right" className="min-w-56">
248
421
  <DropdownMenuGroup>
422
+ <DropdownMenuLabel>{userName}</DropdownMenuLabel>
249
423
  <DropdownMenuItem>
250
424
  <IconUser aria-hidden="true" />
251
425
  Account settings
@@ -262,123 +436,192 @@ function AccountMenu({ userEmail }: { userEmail: string }) {
262
436
  }
263
437
 
264
438
  export function WorkspaceShellRecipe({
265
- title,
266
- workspaceName,
267
- userEmail,
268
439
  activeNavigationKey,
269
- activeSubNavigationKey,
440
+ channelTitle,
270
441
  children,
271
- accessMode = 'client',
442
+ channelMark = <DefaultChannelMark />,
272
443
  defaultSidebarOpen = true,
273
- subNavigation = [],
444
+ defaultSidebarWidth = DEFAULT_SIDEBAR_WIDTH,
445
+ footer,
446
+ headerAction,
447
+ mode = 'standard',
448
+ maxSidebarWidth = MAX_SIDEBAR_WIDTH,
449
+ minSidebarWidth = MIN_SIDEBAR_WIDTH,
450
+ navigationGroups = defaultNavigationGroups,
451
+ pageHeader,
452
+ onSidebarWidthChange,
453
+ userAvatar,
454
+ userName,
455
+ workspaceMark = <DefaultWorkspaceMark />,
456
+ workspaceName,
274
457
  }: WorkspaceShellRecipeProps) {
275
- const darkSidebar = accessMode === 'client'
276
- const sidebarTheme = darkSidebar ? 'dark' : 'light'
458
+ const resolvedMinSidebarWidth = Math.min(minSidebarWidth, maxSidebarWidth)
459
+ const resolvedMaxSidebarWidth = Math.max(minSidebarWidth, maxSidebarWidth)
460
+ const [sidebarWidth, setSidebarWidth] = useState(() =>
461
+ clampSidebarWidth(
462
+ defaultSidebarWidth,
463
+ resolvedMinSidebarWidth,
464
+ resolvedMaxSidebarWidth,
465
+ ),
466
+ )
467
+ const [isSidebarResizing, setIsSidebarResizing] = useState(false)
468
+
469
+ useEffect(() => {
470
+ setSidebarWidth((currentWidth) =>
471
+ clampSidebarWidth(
472
+ currentWidth,
473
+ resolvedMinSidebarWidth,
474
+ resolvedMaxSidebarWidth,
475
+ ),
476
+ )
477
+ }, [resolvedMaxSidebarWidth, resolvedMinSidebarWidth])
478
+
479
+ const updateSidebarWidth = useCallback(
480
+ (nextWidth: number) => {
481
+ const boundedWidth = clampSidebarWidth(
482
+ Math.round(nextWidth),
483
+ resolvedMinSidebarWidth,
484
+ resolvedMaxSidebarWidth,
485
+ )
486
+ if (boundedWidth === sidebarWidth) {
487
+ return
488
+ }
489
+
490
+ setSidebarWidth(boundedWidth)
491
+ onSidebarWidthChange?.(boundedWidth)
492
+ },
493
+ [
494
+ onSidebarWidthChange,
495
+ resolvedMaxSidebarWidth,
496
+ resolvedMinSidebarWidth,
497
+ sidebarWidth,
498
+ ],
499
+ )
500
+
501
+ const resolvedUserAvatar = userAvatar ?? (
502
+ <Avatar size="xs" shape="rounded">
503
+ <AvatarFallback>{initials(userName)}</AvatarFallback>
504
+ </Avatar>
505
+ )
277
506
 
278
507
  return (
279
508
  <TooltipProvider>
280
509
  <SidebarProvider
281
510
  defaultOpen={defaultSidebarOpen}
282
- className="min-h-[720px]"
511
+ data-sidebar-resizing={isSidebarResizing || undefined}
512
+ className="workspace-shell-motion h-svh min-h-[720px] overflow-hidden bg-[#f5f5f5]"
513
+ style={
514
+ {
515
+ '--sidebar-width': `${sidebarWidth}px`,
516
+ '--sidebar-width-icon': '3.4375rem',
517
+ } as ComponentProps<typeof SidebarProvider>['style']
518
+ }
283
519
  >
284
- <Sidebar collapsible="icon">
285
- <div
286
- className={`${darkSidebar ? 'dark' : ''} flex min-h-0 flex-1 flex-col bg-sidebar text-sidebar-foreground`}
287
- data-sidebar-theme={sidebarTheme}
288
- >
289
- <SidebarHeader className="border-b border-sidebar-border p-2">
290
- <WorkspaceSelector workspaceName={workspaceName} />
291
- </SidebarHeader>
292
- <SidebarContent>
293
- <nav aria-label="Primary">
294
- <NavigationGroup
295
- items={primaryNavigation}
296
- activeKey={activeNavigationKey}
297
- />
298
- <NavigationGroup
299
- items={channelNavigation}
300
- activeKey={activeNavigationKey}
301
- label="Channels"
302
- />
520
+ <Sidebar
521
+ collapsible="icon"
522
+ className="group-data-[side=left]:border-r-0"
523
+ >
524
+ <div className="flex min-h-0 flex-1 flex-col bg-sidebar text-sidebar-foreground">
525
+ <SidebarGroup className="h-[76px] shrink-0 flex-row items-center gap-2 p-0 px-[10px] group-data-[collapsible=icon]:px-[15px]">
526
+ <WorkspaceMenu
527
+ workspaceMark={workspaceMark}
528
+ workspaceName={workspaceName}
529
+ />
530
+ <WorkspaceSidebarTrigger />
531
+ </SidebarGroup>
532
+
533
+ <SidebarContent className="pt-2">
534
+ <nav
535
+ aria-label="Workspace"
536
+ className="flex flex-col gap-6 px-[10px]"
537
+ >
538
+ {navigationGroups.map((group) => (
539
+ <NavigationGroup
540
+ key={group.key}
541
+ group={group}
542
+ activeKey={activeNavigationKey}
543
+ />
544
+ ))}
303
545
  </nav>
304
546
  </SidebarContent>
305
- <SidebarFooter className="p-2">
306
- <nav aria-label="Settings">
307
- <SidebarMenu>
308
- <SidebarMenuItem>
309
- <SidebarMenuButton
310
- tooltip="Settings"
311
- isActive={activeNavigationKey === 'settings'}
312
- render={
313
- <RecipeLink
314
- href="#settings"
315
- aria-current={
316
- activeNavigationKey === 'settings'
317
- ? 'page'
318
- : undefined
319
- }
320
- />
321
- }
322
- >
323
- <IconSettings aria-hidden="true" />
324
- <span>Settings</span>
325
- </SidebarMenuButton>
326
- </SidebarMenuItem>
327
- </SidebarMenu>
328
- </nav>
547
+
548
+ <SidebarFooter className="h-14 shrink-0 flex-row items-center gap-2 px-[10px] py-0 group-data-[collapsible=icon]:h-[90px] group-data-[collapsible=icon]:flex-col-reverse group-data-[collapsible=icon]:gap-[10px] group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:py-[22px]">
549
+ <AccountMenu
550
+ userAvatar={resolvedUserAvatar}
551
+ userName={userName}
552
+ />
553
+ <Button
554
+ nativeButton={false}
555
+ render={<RecipeLink href="#settings" />}
556
+ variant="tertiary"
557
+ size="icon-sm"
558
+ aria-label="Settings"
559
+ className="mr-px shrink-0 text-foreground group-data-[collapsible=icon]:mr-0"
560
+ >
561
+ <IconSettings aria-hidden="true" />
562
+ </Button>
329
563
  </SidebarFooter>
330
564
  </div>
331
- <SidebarRail />
565
+ <SidebarRail
566
+ mode="resize"
567
+ value={sidebarWidth}
568
+ min={resolvedMinSidebarWidth}
569
+ max={resolvedMaxSidebarWidth}
570
+ onValueChange={updateSidebarWidth}
571
+ onResizeStart={() => setIsSidebarResizing(true)}
572
+ onResizeEnd={() => setIsSidebarResizing(false)}
573
+ aria-label="Resize workspace navigation"
574
+ className="inset-y-2.5"
575
+ />
332
576
  </Sidebar>
333
577
 
334
- <SidebarInset className="min-w-0">
335
- <header className="sticky top-0 z-20 shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
336
- <div className="grid min-h-16 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-4 transition-[min-height] group-has-data-[collapsible=icon]/sidebar-wrapper:min-h-12 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:px-6">
337
- <div className="col-start-1 row-start-1 flex min-w-0 items-center gap-2">
338
- <SidebarTrigger className="-ml-2 shrink-0" />
339
- <h1 className="truncate text-lg font-semibold tracking-tight">
340
- {title}
341
- </h1>
578
+ <SidebarInset className="min-h-0 min-w-0 bg-[#f5f5f5] p-0 md:p-2.5 md:pl-0">
579
+ <section
580
+ data-slot="workspace-frame"
581
+ data-mode={mode}
582
+ aria-label={channelTitle}
583
+ className="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background md:rounded-[10px]"
584
+ >
585
+ <header className="flex h-14 shrink-0 items-center gap-1 px-4 md:px-5">
586
+ <WorkspaceFrameSidebarTrigger />
587
+ <div className="flex min-w-0 items-center gap-2">
588
+ {channelMark}
589
+ <span className="truncate text-sm font-medium tracking-normal">
590
+ {channelTitle}
591
+ </span>
342
592
  </div>
343
-
344
- {subNavigation.length > 0 && (
345
- <nav
346
- aria-label="Page"
347
- className="col-span-2 col-start-1 row-start-2 -mx-4 overflow-x-auto border-t px-4 md:col-span-1 md:col-start-2 md:row-start-1 md:mx-0 md:overflow-visible md:border-0 md:px-0"
348
- >
349
- <ul className="flex min-w-max items-center justify-center gap-1">
350
- {subNavigation.map((item) => {
351
- const active = item.key === activeSubNavigationKey
352
-
353
- return (
354
- <li key={item.key}>
355
- <Button
356
- aria-current={active ? 'page' : undefined}
357
- nativeButton={false}
358
- render={<RecipeLink href={item.href} />}
359
- role="link"
360
- size="sm"
361
- variant={active ? 'secondary' : 'ghost'}
362
- >
363
- {item.label}
364
- </Button>
365
- </li>
366
- )
367
- })}
368
- </ul>
369
- </nav>
593
+ {headerAction && (
594
+ <div className="ml-auto flex shrink-0 items-center">
595
+ {headerAction}
596
+ </div>
370
597
  )}
598
+ </header>
371
599
 
372
- <div className="col-start-2 row-start-1 flex items-center gap-2 justify-self-end md:col-start-3">
373
- <Button variant="outline" size="sm">
374
- {accessMode === 'client' ? 'Client' : 'AM'}
375
- </Button>
376
- <AccountMenu userEmail={userEmail} />
600
+ {pageHeader && (
601
+ <div data-slot="workspace-page-header" className="shrink-0">
602
+ {pageHeader}
377
603
  </div>
604
+ )}
605
+
606
+ <div
607
+ data-slot="workspace-content"
608
+ className={cn(
609
+ 'min-h-0 flex-1',
610
+ mode === 'focused' ? 'overflow-hidden' : 'overflow-auto',
611
+ )}
612
+ >
613
+ {children}
378
614
  </div>
379
- </header>
380
615
 
381
- <div className="flex-1 p-4 md:p-6">{children}</div>
616
+ {footer && (
617
+ <footer
618
+ data-slot="workspace-footer"
619
+ className="min-h-[88px] shrink-0 border-t bg-background"
620
+ >
621
+ {footer}
622
+ </footer>
623
+ )}
624
+ </section>
382
625
  </SidebarInset>
383
626
  </SidebarProvider>
384
627
  </TooltipProvider>