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