@almanaclabs/blaze-ui 0.3.2 → 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 (133) hide show
  1. package/README.md +23 -0
  2. package/dist/components/accordion/accordion.js +53 -0
  3. package/dist/components/alert/alert.d.ts +1 -1
  4. package/dist/components/alert/alert.js +43 -0
  5. package/dist/components/alert-dialog/alert-dialog.d.ts.map +1 -1
  6. package/dist/components/alert-dialog/alert-dialog.js +94 -0
  7. package/dist/components/aspect-ratio/aspect-ratio.js +13 -0
  8. package/dist/components/attachment/attachment.d.ts +2 -2
  9. package/dist/components/attachment/attachment.js +104 -0
  10. package/dist/components/avatar/avatar.d.ts +6 -4
  11. package/dist/components/avatar/avatar.d.ts.map +1 -1
  12. package/dist/components/avatar/avatar.js +51 -0
  13. package/dist/components/badge/badge.d.ts +3 -2
  14. package/dist/components/badge/badge.d.ts.map +1 -1
  15. package/dist/components/badge/badge.js +45 -0
  16. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  17. package/dist/components/breadcrumb/breadcrumb.js +72 -0
  18. package/dist/components/bubble/bubble.d.ts +1 -1
  19. package/dist/components/bubble/bubble.js +73 -0
  20. package/dist/components/button/button.d.ts +5 -3
  21. package/dist/components/button/button.d.ts.map +1 -1
  22. package/dist/components/button/button.js +65 -0
  23. package/dist/components/button/index.d.ts +1 -1
  24. package/dist/components/button/index.d.ts.map +1 -1
  25. package/dist/components/button-group/button-group.js +41 -0
  26. package/dist/components/calendar/calendar.d.ts.map +1 -1
  27. package/dist/components/calendar/calendar.js +92 -0
  28. package/dist/components/card/card.js +56 -0
  29. package/dist/components/carousel/carousel.d.ts.map +1 -1
  30. package/dist/components/carousel/carousel.js +110 -0
  31. package/dist/components/chart/chart.js +131 -0
  32. package/dist/components/checkbox/checkbox.js +20 -0
  33. package/dist/components/collapsible/collapsible.js +23 -0
  34. package/dist/components/combobox/combobox.d.ts.map +1 -1
  35. package/dist/components/combobox/combobox.js +165 -0
  36. package/dist/components/data-table/data-table.js +30 -0
  37. package/dist/components/date-picker/date-picker.js +39 -0
  38. package/dist/components/dialog/dialog.js +92 -0
  39. package/dist/components/drawer/drawer.js +121 -0
  40. package/dist/components/dropdown-menu/dropdown-menu.js +136 -0
  41. package/dist/components/empty/empty.d.ts.map +1 -1
  42. package/dist/components/empty/empty.js +59 -0
  43. package/dist/components/field/field.d.ts.map +1 -1
  44. package/dist/components/field/field.js +107 -0
  45. package/dist/components/hover-card/hover-card.js +36 -0
  46. package/dist/components/identity-mark/identity-mark.d.ts +5 -0
  47. package/dist/components/identity-mark/identity-mark.d.ts.map +1 -0
  48. package/dist/components/identity-mark/identity-mark.js +20 -0
  49. package/dist/components/identity-mark/index.d.ts +2 -0
  50. package/dist/components/identity-mark/index.d.ts.map +1 -0
  51. package/dist/components/input/input.d.ts +9 -2
  52. package/dist/components/input/input.d.ts.map +1 -1
  53. package/dist/components/input/input.js +28 -0
  54. package/dist/components/input-group/input-group.d.ts +11 -3
  55. package/dist/components/input-group/input-group.d.ts.map +1 -1
  56. package/dist/components/input-group/input-group.js +95 -0
  57. package/dist/components/item/item.d.ts +2 -2
  58. package/dist/components/item/item.js +121 -0
  59. package/dist/components/label/label.js +13 -0
  60. package/dist/components/marker/marker.d.ts +1 -1
  61. package/dist/components/marker/marker.js +43 -0
  62. package/dist/components/message/message.js +49 -0
  63. package/dist/components/metric-overview/index.d.ts +2 -0
  64. package/dist/components/metric-overview/index.d.ts.map +1 -0
  65. package/dist/components/metric-overview/metric-overview.d.ts +12 -0
  66. package/dist/components/metric-overview/metric-overview.d.ts.map +1 -0
  67. package/dist/components/metric-overview/metric-overview.js +42 -0
  68. package/dist/components/native-select/native-select.d.ts +2 -1
  69. package/dist/components/native-select/native-select.d.ts.map +1 -1
  70. package/dist/components/native-select/native-select.js +40 -0
  71. package/dist/components/navigation-action/index.d.ts +2 -0
  72. package/dist/components/navigation-action/index.d.ts.map +1 -0
  73. package/dist/components/navigation-action/navigation-action.d.ts +6 -0
  74. package/dist/components/navigation-action/navigation-action.d.ts.map +1 -0
  75. package/dist/components/navigation-action/navigation-action.js +18 -0
  76. package/dist/components/pagination/pagination.d.ts.map +1 -1
  77. package/dist/components/pagination/pagination.js +80 -0
  78. package/dist/components/popover/popover.js +54 -0
  79. package/dist/components/progress/progress.js +45 -0
  80. package/dist/components/questionnaire/questionnaire-primitive.js +353 -0
  81. package/dist/components/questionnaire/questionnaire.d.ts.map +1 -1
  82. package/dist/components/questionnaire/questionnaire.js +169 -0
  83. package/dist/components/radio-group/radio-group.js +26 -0
  84. package/dist/components/scroll-area/scroll-area.js +37 -0
  85. package/dist/components/select/select.d.ts +2 -1
  86. package/dist/components/select/select.d.ts.map +1 -1
  87. package/dist/components/select/select.js +99 -0
  88. package/dist/components/separator/separator.js +15 -0
  89. package/dist/components/sheet/sheet.js +89 -0
  90. package/dist/components/sidebar/sidebar.d.ts +13 -2
  91. package/dist/components/sidebar/sidebar.d.ts.map +1 -1
  92. package/dist/components/sidebar/sidebar.js +419 -0
  93. package/dist/components/skeleton/skeleton.js +12 -0
  94. package/dist/components/slider/slider.js +37 -0
  95. package/dist/components/spinner/spinner.js +15 -0
  96. package/dist/components/switch/switch.js +19 -0
  97. package/dist/components/table/table.d.ts +4 -1
  98. package/dist/components/table/table.d.ts.map +1 -1
  99. package/dist/components/table/table.js +68 -0
  100. package/dist/components/tabs/tabs.d.ts +1 -1
  101. package/dist/components/tabs/tabs.js +46 -0
  102. package/dist/components/textarea/textarea.js +13 -0
  103. package/dist/components/toast/toast.d.ts.map +1 -1
  104. package/dist/components/toast/toast.js +116 -0
  105. package/dist/components/toggle/toggle.d.ts +1 -1
  106. package/dist/components/toggle/toggle.js +35 -0
  107. package/dist/components/toggle-group/toggle-group.js +52 -0
  108. package/dist/components/tooltip/tooltip.js +41 -0
  109. package/dist/hooks/use-mobile.js +14 -0
  110. package/dist/index.d.ts +4 -1
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +59 -3699
  113. package/dist/lib/control-size.d.ts +9 -0
  114. package/dist/lib/control-size.d.ts.map +1 -0
  115. package/dist/lib/control-size.js +10 -0
  116. package/dist/lib/utils.js +8 -0
  117. package/dist/recipes/recipe-runtime.d.ts +18 -0
  118. package/dist/recipes/recipe-runtime.d.ts.map +1 -0
  119. package/package.json +2 -1
  120. package/recipes/README.md +24 -0
  121. package/recipes/guided-workflow/README.md +51 -0
  122. package/recipes/guided-workflow/guided-workflow.tsx +495 -0
  123. package/recipes/manifest.json +19 -0
  124. package/recipes/workspace-shell/README.md +95 -0
  125. package/recipes/workspace-shell/assets/google-search-ads.svg +6 -0
  126. package/recipes/workspace-shell/assets/home.svg +5 -0
  127. package/recipes/workspace-shell/assets/radiant-health.png +0 -0
  128. package/recipes/workspace-shell/workspace-shell.css +55 -0
  129. package/recipes/workspace-shell/workspace-shell.tsx +636 -0
  130. package/src/styles/fonts/Sohne-Book.woff2 +0 -0
  131. package/src/styles/fonts/Sohne-Medium.woff2 +0 -0
  132. package/src/styles/fonts/Sohne-Semi.woff2 +0 -0
  133. package/src/styles/globals.css +233 -54
@@ -0,0 +1,636 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import type { ComponentProps, ComponentType, ReactNode } from 'react'
5
+ import {
6
+ IconAd2,
7
+ IconChevronDown,
8
+ IconFileAnalytics,
9
+ IconIdBadge,
10
+ IconLayoutSidebarLeftCollapse,
11
+ IconLayoutSidebarLeftExpand,
12
+ IconLogout,
13
+ IconMessageStar,
14
+ IconMessages,
15
+ IconPlugConnected,
16
+ IconScoreboard,
17
+ IconSettings,
18
+ IconUser,
19
+ IconUsers,
20
+ IconWorldSearch,
21
+ IconWorldWww,
22
+ } from '@tabler/icons-react'
23
+
24
+ import {
25
+ Avatar,
26
+ AvatarFallback,
27
+ Button,
28
+ DropdownMenu,
29
+ DropdownMenuContent,
30
+ DropdownMenuGroup,
31
+ DropdownMenuItem,
32
+ DropdownMenuLabel,
33
+ DropdownMenuSeparator,
34
+ DropdownMenuTrigger,
35
+ IdentityMark,
36
+ IdentityMarkImage,
37
+ navigationActionVariants,
38
+ Sidebar,
39
+ SidebarContent,
40
+ SidebarFooter,
41
+ SidebarGroup,
42
+ SidebarGroupContent,
43
+ SidebarGroupLabel,
44
+ SidebarInset,
45
+ SidebarMenu,
46
+ SidebarMenuButton,
47
+ SidebarMenuItem,
48
+ SidebarProvider,
49
+ SidebarRail,
50
+ TooltipProvider,
51
+ cn,
52
+ useSidebar,
53
+ } from '@almanaclabs/blaze-ui'
54
+
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 = {
67
+ className?: string
68
+ 'aria-hidden'?: boolean | 'true' | 'false'
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
+ }
96
+
97
+ export type WorkspaceShellNavigationItem = {
98
+ key: string
99
+ label: string
100
+ href: string
101
+ icon: NavigationIcon
102
+ }
103
+
104
+ export type WorkspaceShellNavigationGroup = {
105
+ key: string
106
+ label: string
107
+ items: readonly WorkspaceShellNavigationItem[]
108
+ }
109
+
110
+ export type WorkspaceShellRecipeProps = {
111
+ activeNavigationKey: string
112
+ channelTitle: string
113
+ children?: ReactNode
114
+ channelMark?: ReactNode
115
+ defaultSidebarOpen?: boolean
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
129
+ }
130
+
131
+ const DEFAULT_SIDEBAR_WIDTH = 220
132
+ const MIN_SIDEBAR_WIDTH = 200
133
+ const MAX_SIDEBAR_WIDTH = 320
134
+
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[] = [
140
+ {
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
+ ],
175
+ },
176
+ {
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
+ ],
211
+ },
212
+ ]
213
+
214
+ function initials(value: string) {
215
+ return value
216
+ .split(/\s+|@/)
217
+ .filter(Boolean)
218
+ .slice(0, 2)
219
+ .map((part) => part[0]?.toUpperCase())
220
+ .join('')
221
+ }
222
+
223
+ function RecipeLink({
224
+ href,
225
+ children,
226
+ ...props
227
+ }: ComponentProps<'a'> & { href: string }) {
228
+ return (
229
+ <a href={href} onClick={(event) => event.preventDefault()} {...props}>
230
+ {children}
231
+ </a>
232
+ )
233
+ }
234
+
235
+ function DefaultWorkspaceMark() {
236
+ return (
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>
350
+ )
351
+ }
352
+
353
+ function NavigationGroup({
354
+ group,
355
+ activeKey,
356
+ }: {
357
+ group: WorkspaceShellNavigationGroup
358
+ activeKey: string
359
+ }) {
360
+ return (
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>
365
+ <SidebarGroupContent>
366
+ <SidebarMenu className="gap-px">
367
+ {group.items.map((item) => (
368
+ <SidebarMenuItem key={item.key}>
369
+ <SidebarMenuButton
370
+ tooltip={item.label}
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
+ )}
377
+ render={
378
+ <RecipeLink
379
+ href={item.href}
380
+ aria-label={item.label}
381
+ aria-current={item.key === activeKey ? 'page' : undefined}
382
+ />
383
+ }
384
+ >
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>
392
+ </SidebarMenuButton>
393
+ </SidebarMenuItem>
394
+ ))}
395
+ </SidebarMenu>
396
+ </SidebarGroupContent>
397
+ </SidebarGroup>
398
+ )
399
+ }
400
+
401
+ function AccountMenu({
402
+ userAvatar,
403
+ userName,
404
+ }: {
405
+ userAvatar: ReactNode
406
+ userName: string
407
+ }) {
408
+ return (
409
+ <DropdownMenu>
410
+ <DropdownMenuTrigger
411
+ render={
412
+ <Button
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"
418
+ />
419
+ }
420
+ >
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>
425
+ </DropdownMenuTrigger>
426
+ <DropdownMenuContent align="start" side="right" className="min-w-56">
427
+ <DropdownMenuGroup>
428
+ <DropdownMenuLabel>{userName}</DropdownMenuLabel>
429
+ <DropdownMenuItem>
430
+ <IconUser aria-hidden="true" />
431
+ Account settings
432
+ </DropdownMenuItem>
433
+ </DropdownMenuGroup>
434
+ <DropdownMenuSeparator />
435
+ <DropdownMenuItem>
436
+ <IconLogout aria-hidden="true" />
437
+ Sign out
438
+ </DropdownMenuItem>
439
+ </DropdownMenuContent>
440
+ </DropdownMenu>
441
+ )
442
+ }
443
+
444
+ export function WorkspaceShellRecipe({
445
+ activeNavigationKey,
446
+ channelTitle,
447
+ children,
448
+ channelMark = <DefaultChannelMark />,
449
+ defaultSidebarOpen = true,
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,
463
+ }: WorkspaceShellRecipeProps) {
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
+ )
512
+
513
+ return (
514
+ <TooltipProvider>
515
+ <SidebarProvider
516
+ defaultOpen={defaultSidebarOpen}
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
+ }
526
+ >
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
+ ))}
552
+ </nav>
553
+ </SidebarContent>
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>
570
+ </SidebarFooter>
571
+ </div>
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
+ />
583
+ </Sidebar>
584
+
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>
599
+ </div>
600
+ {headerAction && (
601
+ <div className="ml-auto flex shrink-0 items-center">
602
+ {headerAction}
603
+ </div>
604
+ )}
605
+ </header>
606
+
607
+ {pageHeader && (
608
+ <div data-slot="workspace-page-header" className="shrink-0">
609
+ {pageHeader}
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}
621
+ </div>
622
+
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>
632
+ </SidebarInset>
633
+ </SidebarProvider>
634
+ </TooltipProvider>
635
+ )
636
+ }