@flamingo-stack/openframe-frontend-core 0.0.390 → 0.0.391-snapshot.20260709132828

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 (141) hide show
  1. package/dist/{chunk-RAN67OX4.js → chunk-2KW2GOFW.js} +2394 -2203
  2. package/dist/chunk-2KW2GOFW.js.map +1 -0
  3. package/dist/{chunk-3DTVUZUS.cjs → chunk-2Q2PIMSX.cjs} +5 -5
  4. package/dist/{chunk-3DTVUZUS.cjs.map → chunk-2Q2PIMSX.cjs.map} +1 -1
  5. package/dist/{chunk-W4T6Z27I.cjs → chunk-2RCCOZHC.cjs} +15 -15
  6. package/dist/{chunk-W4T6Z27I.cjs.map → chunk-2RCCOZHC.cjs.map} +1 -1
  7. package/dist/{chunk-XR4SQ32H.cjs → chunk-3YBDFKBZ.cjs} +1953 -1762
  8. package/dist/chunk-3YBDFKBZ.cjs.map +1 -0
  9. package/dist/{chunk-PQY63H3D.js → chunk-45IP2W4W.js} +2 -2
  10. package/dist/{chunk-ZT56IO5B.js → chunk-4L4JG5GB.js} +2 -2
  11. package/dist/{chunk-DI55URRP.js → chunk-4ZCXV6GB.js} +4 -4
  12. package/dist/{chunk-PY376F37.cjs → chunk-5PF4BSCV.cjs} +12 -12
  13. package/dist/{chunk-PY376F37.cjs.map → chunk-5PF4BSCV.cjs.map} +1 -1
  14. package/dist/{chunk-ZPMRWBWF.js → chunk-AGZMMTRL.js} +4 -4
  15. package/dist/{chunk-NIBFE2DV.cjs → chunk-ANLFC67D.cjs} +37 -37
  16. package/dist/{chunk-NIBFE2DV.cjs.map → chunk-ANLFC67D.cjs.map} +1 -1
  17. package/dist/{chunk-IDDSEQBZ.js → chunk-AQUU74K4.js} +4 -4
  18. package/dist/{chunk-3BNQCRMN.js → chunk-CSHLDG7J.js} +2 -2
  19. package/dist/{chunk-L5HOLKHH.cjs → chunk-DHJVMIKE.cjs} +26 -26
  20. package/dist/{chunk-L5HOLKHH.cjs.map → chunk-DHJVMIKE.cjs.map} +1 -1
  21. package/dist/{chunk-HWH2ZTP3.cjs → chunk-FGZ23YLT.cjs} +62 -62
  22. package/dist/{chunk-HWH2ZTP3.cjs.map → chunk-FGZ23YLT.cjs.map} +1 -1
  23. package/dist/{chunk-EFY7ALPU.cjs → chunk-GCUAXBR6.cjs} +9 -9
  24. package/dist/{chunk-EFY7ALPU.cjs.map → chunk-GCUAXBR6.cjs.map} +1 -1
  25. package/dist/{chunk-LTGZ46D4.js → chunk-HXFHEKGD.js} +4 -4
  26. package/dist/{chunk-IBEJAPAU.js → chunk-J6QEZIRO.js} +2 -2
  27. package/dist/{chunk-XG4FQFMH.cjs → chunk-NYAZENBC.cjs} +7 -7
  28. package/dist/{chunk-XG4FQFMH.cjs.map → chunk-NYAZENBC.cjs.map} +1 -1
  29. package/dist/{chunk-DAMGTJ7E.js → chunk-QEN2T5AV.js} +2 -2
  30. package/dist/{chunk-MSKUU2C6.js → chunk-R2VRTPLY.js} +2 -2
  31. package/dist/{chunk-33IT4ZAY.js → chunk-SGKMMZBD.js} +2 -2
  32. package/dist/{chunk-TOBDMXND.cjs → chunk-TTL3ILIM.cjs} +31 -31
  33. package/dist/{chunk-TOBDMXND.cjs.map → chunk-TTL3ILIM.cjs.map} +1 -1
  34. package/dist/{chunk-RABSUY3W.cjs → chunk-UCXBVKSM.cjs} +26 -26
  35. package/dist/{chunk-RABSUY3W.cjs.map → chunk-UCXBVKSM.cjs.map} +1 -1
  36. package/dist/{chunk-EIH6WUAP.cjs → chunk-YV4UMWDP.cjs} +59 -59
  37. package/dist/{chunk-EIH6WUAP.cjs.map → chunk-YV4UMWDP.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/approval-batch-message.d.ts +13 -4
  41. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  42. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  44. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  45. package/dist/components/chat/index.cjs +4 -2
  46. package/dist/components/chat/index.cjs.map +1 -1
  47. package/dist/components/chat/index.js +3 -1
  48. package/dist/components/chat/types/chat.types.d.ts +13 -0
  49. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  50. package/dist/components/chat/types/component.types.d.ts +21 -3
  51. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  52. package/dist/components/chat/types/message.types.d.ts +3 -0
  53. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  54. package/dist/components/contact/index.cjs +3 -3
  55. package/dist/components/contact/index.js +2 -2
  56. package/dist/components/docs/index.cjs +5 -5
  57. package/dist/components/docs/index.js +4 -4
  58. package/dist/components/embeds/index.cjs +3 -3
  59. package/dist/components/embeds/index.js +2 -2
  60. package/dist/components/faq/index.cjs +3 -3
  61. package/dist/components/faq/index.js +2 -2
  62. package/dist/components/features/auth/auth-shell.d.ts +1 -3
  63. package/dist/components/features/auth/auth-shell.d.ts.map +1 -1
  64. package/dist/components/features/auth/complete-account-form.d.ts +43 -0
  65. package/dist/components/features/auth/complete-account-form.d.ts.map +1 -0
  66. package/dist/components/features/auth/index.d.ts +1 -1
  67. package/dist/components/features/auth/index.d.ts.map +1 -1
  68. package/dist/components/features/index.cjs +2 -2
  69. package/dist/components/features/index.cjs.map +1 -1
  70. package/dist/components/features/index.js +3 -3
  71. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  72. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +5 -2
  73. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
  74. package/dist/components/help-center-pages/index.cjs +20 -20
  75. package/dist/components/help-center-pages/index.js +11 -11
  76. package/dist/components/index.cjs +122 -120
  77. package/dist/components/index.cjs.map +1 -1
  78. package/dist/components/index.js +13 -11
  79. package/dist/components/index.js.map +1 -1
  80. package/dist/components/navigation/app-header.d.ts.map +1 -1
  81. package/dist/components/navigation/app-layout-context.d.ts +34 -0
  82. package/dist/components/navigation/app-layout-context.d.ts.map +1 -0
  83. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  84. package/dist/components/navigation/app-layout.d.ts +2 -21
  85. package/dist/components/navigation/app-layout.d.ts.map +1 -1
  86. package/dist/components/navigation/index.cjs +2 -2
  87. package/dist/components/navigation/index.js +1 -1
  88. package/dist/components/onboarding-guides/index.cjs +5 -5
  89. package/dist/components/onboarding-guides/index.js +4 -4
  90. package/dist/components/related-content/index.cjs +3 -3
  91. package/dist/components/related-content/index.js +2 -2
  92. package/dist/components/tickets/index.cjs +5 -5
  93. package/dist/components/tickets/index.js +4 -4
  94. package/dist/components/ui/index.cjs +4 -2
  95. package/dist/components/ui/index.cjs.map +1 -1
  96. package/dist/components/ui/index.js +3 -1
  97. package/dist/index.cjs +4 -2
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.js +5 -3
  100. package/dist/utils/index.cjs.map +1 -1
  101. package/dist/utils/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/components/chat/approval-batch-message.tsx +104 -88
  104. package/src/components/chat/approval-request-message.tsx +57 -3
  105. package/src/components/chat/chat-message-enhanced.tsx +16 -2
  106. package/src/components/chat/chat-message-list.tsx +3 -0
  107. package/src/components/chat/types/chat.types.ts +17 -0
  108. package/src/components/chat/types/component.types.ts +21 -3
  109. package/src/components/chat/types/message.types.ts +3 -0
  110. package/src/components/features/auth/auth-shell.tsx +2 -11
  111. package/src/components/features/auth/complete-account-form.tsx +197 -0
  112. package/src/components/features/auth/index.ts +1 -1
  113. package/src/components/features/notifications/notification-drawer.tsx +72 -43
  114. package/src/components/features/time-tracker/time-tracker-header-button.tsx +46 -6
  115. package/src/components/navigation/app-header.tsx +13 -9
  116. package/src/components/navigation/app-layout-context.ts +46 -0
  117. package/src/components/navigation/app-layout-drawer.tsx +21 -11
  118. package/src/components/navigation/app-layout.tsx +17 -39
  119. package/src/stories/ApprovalBatchMessageClient.stories.tsx +20 -62
  120. package/src/stories/auth/CompleteAccount.stories.tsx +103 -0
  121. package/src/stories/auth/CreateOrganization.stories.tsx +0 -6
  122. package/src/stories/auth/Login.stories.tsx +0 -6
  123. package/src/stories/auth/PasswordReset.stories.tsx +0 -6
  124. package/src/styles/app-globals.css +30 -17
  125. package/dist/chunk-RAN67OX4.js.map +0 -1
  126. package/dist/chunk-XR4SQ32H.cjs.map +0 -1
  127. package/dist/components/features/auth/accept-invitation-form.d.ts +0 -23
  128. package/dist/components/features/auth/accept-invitation-form.d.ts.map +0 -1
  129. package/src/components/features/auth/accept-invitation-form.tsx +0 -71
  130. package/src/stories/auth/AcceptInvitation.stories.tsx +0 -54
  131. /package/dist/{chunk-PQY63H3D.js.map → chunk-45IP2W4W.js.map} +0 -0
  132. /package/dist/{chunk-ZT56IO5B.js.map → chunk-4L4JG5GB.js.map} +0 -0
  133. /package/dist/{chunk-DI55URRP.js.map → chunk-4ZCXV6GB.js.map} +0 -0
  134. /package/dist/{chunk-ZPMRWBWF.js.map → chunk-AGZMMTRL.js.map} +0 -0
  135. /package/dist/{chunk-IDDSEQBZ.js.map → chunk-AQUU74K4.js.map} +0 -0
  136. /package/dist/{chunk-3BNQCRMN.js.map → chunk-CSHLDG7J.js.map} +0 -0
  137. /package/dist/{chunk-LTGZ46D4.js.map → chunk-HXFHEKGD.js.map} +0 -0
  138. /package/dist/{chunk-IBEJAPAU.js.map → chunk-J6QEZIRO.js.map} +0 -0
  139. /package/dist/{chunk-DAMGTJ7E.js.map → chunk-QEN2T5AV.js.map} +0 -0
  140. /package/dist/{chunk-MSKUU2C6.js.map → chunk-R2VRTPLY.js.map} +0 -0
  141. /package/dist/{chunk-33IT4ZAY.js.map → chunk-SGKMMZBD.js.map} +0 -0
@@ -0,0 +1,197 @@
1
+ 'use client'
2
+
3
+ import type * as React from 'react'
4
+ import { cn } from '../../../utils/cn'
5
+ import { Button } from '../../ui/button'
6
+ import { Input } from '../../ui/input'
7
+ import { PasswordInput } from '../../ui/password-input'
8
+ import type { AuthSsoProvider } from './sso-providers'
9
+ import { SsoProviderButtons } from './sso-providers'
10
+
11
+ export interface CompleteAccountFormProps {
12
+ /** Controlled field values */
13
+ firstName: string
14
+ lastName: string
15
+ password: string
16
+ confirmPassword: string
17
+ onFirstNameChange: (value: string) => void
18
+ onLastNameChange: (value: string) => void
19
+ onPasswordChange: (value: string) => void
20
+ onConfirmPasswordChange: (value: string) => void
21
+ /** Primary submit ("Start Free Trial") */
22
+ onSubmit: () => void
23
+ /** Secondary action rendered left of the submit (e.g. "Back to Organization"). */
24
+ onBack?: () => void
25
+ /** SSO alternatives offered above the fields ("Continue with …"). */
26
+ ssoProviders?: AuthSsoProvider[]
27
+ onSsoClick?: (provider: AuthSsoProvider) => void
28
+ title?: string
29
+ subtitle?: string
30
+ dividerLabel?: string
31
+ submitLabel?: string
32
+ backLabel?: string
33
+ ssoActionLabel?: string
34
+ submitDisabled?: boolean
35
+ loading?: boolean
36
+ disabled?: boolean
37
+ errors?: {
38
+ firstName?: string
39
+ lastName?: string
40
+ password?: string
41
+ confirmPassword?: string
42
+ }
43
+ className?: string
44
+ }
45
+
46
+ /**
47
+ * Account details form shared by the Sign Up ("Complete your Account") and
48
+ * Accept Invitation screens. Presentational + controlled — SSO shortcuts on
49
+ * top, then name + password fields ("or create account").
50
+ */
51
+ export function CompleteAccountForm({
52
+ firstName,
53
+ lastName,
54
+ password,
55
+ confirmPassword,
56
+ onFirstNameChange,
57
+ onLastNameChange,
58
+ onPasswordChange,
59
+ onConfirmPasswordChange,
60
+ onSubmit,
61
+ onBack,
62
+ ssoProviders,
63
+ onSsoClick,
64
+ title = 'Complete your Account',
65
+ subtitle = 'Fill in the details below to get started',
66
+ dividerLabel = 'or create account',
67
+ submitLabel = 'Start Free Trial',
68
+ backLabel = 'Back to Organization',
69
+ ssoActionLabel = 'Continue with',
70
+ submitDisabled = false,
71
+ loading = false,
72
+ disabled = false,
73
+ errors,
74
+ className,
75
+ }: CompleteAccountFormProps) {
76
+ const fieldsDisabled = disabled || loading
77
+
78
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
79
+ if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
80
+ onSubmit()
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div
86
+ className={cn(
87
+ 'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',
88
+ className,
89
+ )}
90
+ >
91
+ {/* Header */}
92
+ <div className="flex flex-col">
93
+ <h1 className="text-h2 text-ods-text-primary tracking-[-0.64px]">{title}</h1>
94
+ <p className="text-h4 text-ods-text-secondary">{subtitle}</p>
95
+ </div>
96
+
97
+ {/* SSO shortcuts + divider */}
98
+ {ssoProviders && ssoProviders.length > 0 && (
99
+ <>
100
+ <SsoProviderButtons
101
+ providers={ssoProviders}
102
+ onSsoClick={onSsoClick}
103
+ actionLabel={ssoActionLabel}
104
+ disabled={fieldsDisabled}
105
+ />
106
+ <div className="flex items-center gap-[var(--spacing-system-s)]">
107
+ <div className="h-px flex-1 bg-ods-border" />
108
+ <span className="text-h6 text-ods-text-secondary">{dividerLabel}</span>
109
+ <div className="h-px flex-1 bg-ods-border" />
110
+ </div>
111
+ </>
112
+ )}
113
+
114
+ {/* First + Last name */}
115
+ <div className="flex gap-[var(--spacing-system-l)]">
116
+ <div className="min-w-0 flex-1">
117
+ <Input
118
+ label="First Name"
119
+ placeholder="Enter First Name"
120
+ value={firstName}
121
+ error={errors?.firstName}
122
+ disabled={fieldsDisabled}
123
+ onChange={(event) => onFirstNameChange(event.target.value)}
124
+ onKeyDown={handleKeyDown}
125
+ />
126
+ </div>
127
+ <div className="min-w-0 flex-1">
128
+ <Input
129
+ label="Last Name"
130
+ placeholder="Enter Last Name"
131
+ value={lastName}
132
+ error={errors?.lastName}
133
+ disabled={fieldsDisabled}
134
+ onChange={(event) => onLastNameChange(event.target.value)}
135
+ onKeyDown={handleKeyDown}
136
+ />
137
+ </div>
138
+ </div>
139
+
140
+ {/* Password + Confirm */}
141
+ <div className="flex gap-[var(--spacing-system-l)]">
142
+ <div className="min-w-0 flex-1">
143
+ <PasswordInput
144
+ label="Password"
145
+ placeholder="Enter Password"
146
+ value={password}
147
+ error={errors?.password}
148
+ disabled={fieldsDisabled}
149
+ onChange={(event) => onPasswordChange(event.target.value)}
150
+ onKeyDown={handleKeyDown}
151
+ />
152
+ </div>
153
+ <div className="min-w-0 flex-1">
154
+ <PasswordInput
155
+ label="Confirm Password"
156
+ placeholder="Confirm Password"
157
+ value={confirmPassword}
158
+ error={errors?.confirmPassword}
159
+ disabled={fieldsDisabled}
160
+ onChange={(event) => onConfirmPasswordChange(event.target.value)}
161
+ onKeyDown={handleKeyDown}
162
+ />
163
+ </div>
164
+ </div>
165
+
166
+ {/* Actions — optional back + submit */}
167
+ <div className="flex items-center gap-[var(--spacing-system-l)]">
168
+ {onBack ? (
169
+ <Button
170
+ type="button"
171
+ variant="transparent"
172
+ fullWidth
173
+ className="flex-1"
174
+ disabled={fieldsDisabled}
175
+ onClick={onBack}
176
+ >
177
+ {backLabel}
178
+ </Button>
179
+ ) : (
180
+ // Spacer keeps the button on the right half, matching the design
181
+ <div className="flex-1" />
182
+ )}
183
+ <Button
184
+ type="button"
185
+ variant="accent"
186
+ fullWidth
187
+ className="flex-1"
188
+ loading={loading}
189
+ disabled={disabled || submitDisabled}
190
+ onClick={onSubmit}
191
+ >
192
+ {submitLabel}
193
+ </Button>
194
+ </div>
195
+ </div>
196
+ )
197
+ }
@@ -1,8 +1,8 @@
1
- export * from './accept-invitation-form'
2
1
  export * from './auth-benefits-panel'
3
2
  export * from './auth-branding'
4
3
  export * from './auth-shell'
5
4
  export * from './back-to-login-link'
5
+ export * from './complete-account-form'
6
6
  export * from './create-organization-form'
7
7
  export * from './invite-link-invalid-modal'
8
8
  export * from './login-form'
@@ -3,6 +3,8 @@
3
3
  import { useEffect, useRef } from 'react'
4
4
  import { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon'
5
5
  import { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated'
6
+ import { useAppLayoutDrawerContainer } from '../../navigation/app-layout-context'
7
+ import { AppLayoutDrawer, AppLayoutDrawerContent } from '../../navigation/app-layout-drawer'
6
8
  import { SplitButton } from '../../ui/button'
7
9
  import { Drawer, DrawerContent, DrawerTitle } from '../../ui/drawer'
8
10
  import { Switch } from '../../ui/switch'
@@ -27,6 +29,10 @@ export function NotificationDrawer({
27
29
  renderTile: renderTileProp,
28
30
  }: NotificationDrawerProps) {
29
31
  const ctx = useOptionalNotifications()
32
+ // Inside AppLayout the drawer renders in-layout (dims and covers only the
33
+ // main content area — header and sidebar stay interactive). Outside of it
34
+ // (Storybook, non-AppLayout hosts) it falls back to the viewport Drawer.
35
+ const layoutContainer = useAppLayoutDrawerContainer()
30
36
 
31
37
  if (!ctx) return null
32
38
 
@@ -56,6 +62,71 @@ export function NotificationDrawer({
56
62
 
57
63
  const unreadNotifications = notifications.filter((n) => !n.read)
58
64
 
65
+ const content = (
66
+ <>
67
+ <div className="px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]">
68
+ <DrawerTitle
69
+ hideClose
70
+ className="truncate"
71
+ actions={
72
+ <button
73
+ type="button"
74
+ disabled={unreadCount === 0}
75
+ onClick={markAllRead}
76
+ className="self-center text-h6 text-ods-text-secondary underline transition-colors hover:text-ods-text-primary disabled:opacity-40"
77
+ >
78
+ Mark All Complete
79
+ </button>
80
+ }
81
+ >
82
+ New Notifications
83
+ </DrawerTitle>
84
+ </div>
85
+
86
+ <DrawerScrollList
87
+ unreadNotifications={unreadNotifications}
88
+ liveDurationMs={liveDurationMs}
89
+ onComplete={markRead}
90
+ onSettle={markSettled}
91
+ hasMore={hasMore}
92
+ isLoadingMore={isLoadingMore}
93
+ loadMore={loadMore}
94
+ loadMoreRootMargin={loadMoreRootMargin}
95
+ renderTile={renderTile}
96
+ />
97
+
98
+ <div className="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
99
+ <div className="overflow-hidden rounded-md border border-ods-border bg-ods-card">
100
+ <ShowNotificationsToggleRow checked={showPopups} onChange={setShowPopups} />
101
+ {desktopPopupsConfigured && (
102
+ <DesktopNotificationsToggleRow checked={showDesktopPopups} onChange={setShowDesktopPopups} />
103
+ )}
104
+ </div>
105
+ <NotificationsHistoryButton
106
+ onClick={onHistoryClick ? () => { onHistoryClick(); close() } : undefined}
107
+ historyHref={historyHref}
108
+ />
109
+ </div>
110
+ </>
111
+ )
112
+
113
+ if (layoutContainer) {
114
+ return (
115
+ <AppLayoutDrawer open={isOpen} onOpenChange={(v) => (v ? open() : close())}>
116
+ <AppLayoutDrawerContent
117
+ side="right"
118
+ aria-describedby={undefined}
119
+ // md matches the content's default mobileBreakpoint: below it the
120
+ // panel is forced full-bleed (w-full), so the fixed width must not
121
+ // apply there or the panel would detach from the right edge.
122
+ className={cn('md:w-[26rem] gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)]', className)}
123
+ >
124
+ {content}
125
+ </AppLayoutDrawerContent>
126
+ </AppLayoutDrawer>
127
+ )
128
+ }
129
+
59
130
  return (
60
131
  <Drawer open={isOpen} onOpenChange={(v) => (v ? open() : close())}>
61
132
  <DrawerContent
@@ -67,49 +138,7 @@ export function NotificationDrawer({
67
138
  className,
68
139
  )}
69
140
  >
70
- <div className="px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]">
71
- <DrawerTitle
72
- hideClose
73
- className="truncate"
74
- actions={
75
- <button
76
- type="button"
77
- disabled={unreadCount === 0}
78
- onClick={markAllRead}
79
- className="self-center text-h6 text-ods-text-secondary underline transition-colors hover:text-ods-text-primary disabled:opacity-40"
80
- >
81
- Complete All
82
- </button>
83
- }
84
- >
85
- New Notifications
86
- </DrawerTitle>
87
- </div>
88
-
89
- <DrawerScrollList
90
- unreadNotifications={unreadNotifications}
91
- liveDurationMs={liveDurationMs}
92
- onComplete={markRead}
93
- onSettle={markSettled}
94
- hasMore={hasMore}
95
- isLoadingMore={isLoadingMore}
96
- loadMore={loadMore}
97
- loadMoreRootMargin={loadMoreRootMargin}
98
- renderTile={renderTile}
99
- />
100
-
101
- <div className="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
102
- <div className="overflow-hidden rounded-md border border-ods-border bg-ods-card">
103
- <ShowNotificationsToggleRow checked={showPopups} onChange={setShowPopups} />
104
- {desktopPopupsConfigured && (
105
- <DesktopNotificationsToggleRow checked={showDesktopPopups} onChange={setShowDesktopPopups} />
106
- )}
107
- </div>
108
- <NotificationsHistoryButton
109
- onClick={onHistoryClick ? () => { onHistoryClick(); close() } : undefined}
110
- historyHref={historyHref}
111
- />
112
- </div>
141
+ {content}
113
142
  </DrawerContent>
114
143
  </Drawer>
115
144
  )
@@ -1,8 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import * as PopoverPrimitive from '@radix-ui/react-popover'
4
+ import { useEffect, useRef } from 'react'
4
5
  import { cn } from '../../../utils/cn'
5
6
  import { ClockHistoryIcon } from '../../icons-v2-generated/date-and-time/clock-history-icon'
7
+ import {
8
+ type AppLayoutDrawerHandle,
9
+ useAppLayoutDrawerContainer,
10
+ useAppLayoutDrawerCoordination,
11
+ } from '../../navigation/app-layout-context'
6
12
  import { HeaderButton } from '../../navigation/header-button'
7
13
  import { OVERLAY_BACKDROP_CLASS } from '../../ui/drawer'
8
14
  import { TimeTrackerPanel } from './time-tracker-panel'
@@ -19,8 +25,11 @@ export interface TimeTrackerHeaderButtonProps {
19
25
  * Header affordance for the time tracker. Renders nothing unless wrapped in a
20
26
  * `<TimeTrackerProvider>`. The trigger is the standard `HeaderButton`; when a
21
27
  * session is active it also shows the live elapsed time. The popup is a Radix
22
- * Popover anchored under the button (not a modal/drawer), backed by the same
23
- * dim overlay as the Drawer so all panels darken the page identically.
28
+ * Popover anchored under the button (not a modal/drawer). Inside AppLayout the
29
+ * dim backdrop covers only the main content area (header and sidebar stay
30
+ * interactive) and the popup joins the layout's panel coordination — opening
31
+ * it closes any open in-layout drawer and vice versa. Outside AppLayout it
32
+ * falls back to a viewport-wide backdrop.
24
33
  */
25
34
  export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHeaderButtonProps) {
26
35
  const ctx = useOptionalTimeTracker()
@@ -29,10 +38,38 @@ export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHead
29
38
  runningSince: ctx?.runningSince,
30
39
  accumulatedMs: ctx?.accumulatedMs,
31
40
  })
41
+ const layoutContainer = useAppLayoutDrawerContainer()
42
+ const coordination = useAppLayoutDrawerCoordination()
43
+
44
+ const isOpen = ctx?.isOpen ?? false
45
+ const isOpenRef = useRef(isOpen)
46
+ isOpenRef.current = isOpen
47
+ const closeRef = useRef(ctx?.close)
48
+ closeRef.current = ctx?.close
49
+ // Stable handle: the same object must be registered AND passed as `self`
50
+ // to notifyDrawerDidOpen so the coordinator can skip it when closing the
51
+ // other panels.
52
+ const selfHandleRef = useRef<AppLayoutDrawerHandle | null>(null)
53
+ if (!selfHandleRef.current) {
54
+ selfHandleRef.current = {
55
+ close: () => {
56
+ if (isOpenRef.current) closeRef.current?.()
57
+ },
58
+ }
59
+ }
60
+
61
+ useEffect(() => {
62
+ if (isOpen) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined)
63
+ }, [isOpen, coordination])
64
+
65
+ useEffect(() => {
66
+ if (!selfHandleRef.current) return
67
+ return coordination?.registerDrawer(selfHandleRef.current)
68
+ }, [coordination])
32
69
 
33
70
  if (!ctx) return null
34
71
 
35
- const { isOpen, open, close, status } = ctx
72
+ const { open, close, status } = ctx
36
73
  const isPaused = status === 'paused'
37
74
  const isActive = status === 'tracking' || isPaused
38
75
 
@@ -72,13 +109,16 @@ export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHead
72
109
  </PopoverPrimitive.Trigger>
73
110
  {/* Popovers have no built-in overlay — portal a dim backdrop behind the
74
111
  panel. Clicks land on it (outside Content), so Radix still closes the
75
- popover. Own Portal: Radix's Portal slots a single child. */}
76
- <PopoverPrimitive.Portal>
112
+ popover. Own Portal: Radix's Portal slots a single child. Inside
113
+ AppLayout the backdrop portals into the main-area container (absolute,
114
+ AppLayoutDrawer-overlay tier) so header/sidebar stay undimmed and
115
+ interactive; standalone it dims the whole viewport. */}
116
+ <PopoverPrimitive.Portal container={layoutContainer ?? undefined}>
77
117
  <div
78
118
  aria-hidden="true"
79
119
  data-state={isOpen ? 'open' : 'closed'}
80
120
  className={cn(
81
- 'fixed inset-0 z-[1299]',
121
+ layoutContainer ? 'absolute inset-0 z-[102]' : 'fixed inset-0 z-[1299]',
82
122
  OVERLAY_BACKDROP_CLASS,
83
123
  'data-[state=open]:animate-in data-[state=open]:fade-in-0',
84
124
  'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
@@ -251,16 +251,20 @@ function NotificationsHeaderButton({
251
251
  return (
252
252
  <HeaderButton
253
253
  icon={
254
- <div className="relative w-4 h-4 md:w-6 md:h-6">
255
- <BellIcon className="w-full h-full" />
256
- {hasUnread && (
257
- <span
258
- className="absolute top-0 right-0 bg-ods-warning rounded-full w-1.5 h-1.5 md:w-2 md:h-2"
259
- />
260
- )}
261
- </div>
254
+ isActive ? (
255
+ <XmarkIcon className="w-4 h-4 md:w-6 md:h-6" />
256
+ ) : (
257
+ <div className="relative w-4 h-4 md:w-6 md:h-6">
258
+ <BellIcon className="w-full h-full" />
259
+ {hasUnread && (
260
+ <span
261
+ className="absolute top-0 right-0 bg-ods-warning rounded-full w-1.5 h-1.5 md:w-2 md:h-2"
262
+ />
263
+ )}
264
+ </div>
265
+ )
262
266
  }
263
- aria-label="Notifications"
267
+ aria-label={isActive ? 'Close notifications' : 'Notifications'}
264
268
  onClick={onClick}
265
269
  isActive={isActive}
266
270
  disabled={disabled || !onClick}
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext } from 'react'
4
+
5
+ /**
6
+ * Container element that wraps `<main>` and serves as the portal target for
7
+ * `AppLayoutDrawer`. Drawers rendered into this container sit on top of the
8
+ * main content area only — the sidebar and header remain visible and
9
+ * interactive. Null when AppLayout hasn't mounted (or when used outside of it).
10
+ *
11
+ * Lives in its own module (not app-layout.tsx) so in-layout panels
12
+ * (AppLayoutDrawer, NotificationDrawer, TimeTrackerHeaderButton) can consume
13
+ * these contexts without importing AppLayout itself — AppLayout renders those
14
+ * panels, so importing it back would create a module cycle.
15
+ */
16
+ export const AppLayoutDrawerContainerContext = createContext<HTMLElement | null>(null)
17
+
18
+ export function useAppLayoutDrawerContainer(): HTMLElement | null {
19
+ return useContext(AppLayoutDrawerContainerContext)
20
+ }
21
+
22
+ /**
23
+ * Coordination between AppLayout's in-layout panels (`AppLayoutDrawer`, the
24
+ * time-tracker popup) and the mobile burger menu. At most ONE of them may be
25
+ * open at a time — each dims the main area, so stacking them reads as broken:
26
+ * - a panel that opens calls `notifyDrawerDidOpen(self)` so the layout
27
+ * closes the burger menu AND every other registered panel;
28
+ * - each panel registers a close handle so opening the menu via the burger
29
+ * button (or another panel) closes it. Null outside of AppLayout.
30
+ */
31
+ export interface AppLayoutDrawerHandle {
32
+ close: () => void
33
+ }
34
+
35
+ export interface AppLayoutDrawerCoordination {
36
+ /** `self` is skipped when closing the other registered panels. */
37
+ notifyDrawerDidOpen: (self?: AppLayoutDrawerHandle) => void
38
+ /** Returns an unregister cleanup. */
39
+ registerDrawer: (handle: AppLayoutDrawerHandle) => () => void
40
+ }
41
+
42
+ export const AppLayoutDrawerCoordinationContext = createContext<AppLayoutDrawerCoordination | null>(null)
43
+
44
+ export function useAppLayoutDrawerCoordination(): AppLayoutDrawerCoordination | null {
45
+ return useContext(AppLayoutDrawerCoordinationContext)
46
+ }
@@ -14,7 +14,11 @@ import {
14
14
  OVERLAY_BACKDROP_CLASS,
15
15
  type DrawerSide,
16
16
  } from "../ui/drawer"
17
- import { useAppLayoutDrawerContainer, useAppLayoutDrawerCoordination } from "./app-layout"
17
+ import {
18
+ type AppLayoutDrawerHandle,
19
+ useAppLayoutDrawerContainer,
20
+ useAppLayoutDrawerCoordination,
21
+ } from "./app-layout-context"
18
22
 
19
23
  /**
20
24
  * AppLayoutDrawer is a Drawer variant that renders **inside** AppLayout's main
@@ -68,26 +72,32 @@ const AppLayoutDrawerRoot = ({
68
72
  [isControlled, onOpenChange],
69
73
  )
70
74
 
71
- // Coordinate with AppLayout's mobile burger menu (the drawer covers it):
72
- // opening the drawer closes the menu, and opening the menu closes the
73
- // drawer via the registered close handle. Refs keep the registration
74
- // effect independent of render-to-render identity changes.
75
+ // Coordinate with AppLayout's other in-layout panels and the mobile burger
76
+ // menu: opening this drawer closes them, and opening any of them closes
77
+ // this drawer via the registered close handle. Refs keep the handle
78
+ // identity stable across renders the same object must be registered AND
79
+ // passed as `self` so the coordinator can skip it when closing the rest.
75
80
  const coordination = useAppLayoutDrawerCoordination()
76
81
  const openRef = React.useRef(open)
77
82
  openRef.current = open
78
83
  const handleOpenChangeRef = React.useRef(handleOpenChange)
79
84
  handleOpenChangeRef.current = handleOpenChange
85
+ const selfHandleRef = React.useRef<AppLayoutDrawerHandle | null>(null)
86
+ if (!selfHandleRef.current) {
87
+ selfHandleRef.current = {
88
+ close: () => {
89
+ if (openRef.current) handleOpenChangeRef.current(false)
90
+ },
91
+ }
92
+ }
80
93
 
81
94
  React.useEffect(() => {
82
- if (open) coordination?.notifyDrawerDidOpen()
95
+ if (open) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined)
83
96
  }, [open, coordination])
84
97
 
85
98
  React.useEffect(() => {
86
- return coordination?.registerDrawer({
87
- close: () => {
88
- if (openRef.current) handleOpenChangeRef.current(false)
89
- },
90
- })
99
+ if (!selfHandleRef.current) return
100
+ return coordination?.registerDrawer(selfHandleRef.current)
91
101
  }, [coordination])
92
102
 
93
103
  return (
@@ -1,50 +1,21 @@
1
1
  'use client'
2
2
 
3
- import { createContext, Suspense, useCallback, useContext, useMemo, useRef, useState } from 'react'
3
+ import { Suspense, useCallback, useMemo, useRef, useState } from 'react'
4
4
  import { NavigationSidebarConfig } from '../../types/navigation'
5
5
  import { cn } from '../../utils'
6
6
  import { NotificationDrawer } from '../features/notifications/notification-drawer'
7
7
  import { AppHeader, AppHeaderProps } from './app-header'
8
+ import {
9
+ AppLayoutDrawerContainerContext,
10
+ type AppLayoutDrawerCoordination,
11
+ AppLayoutDrawerCoordinationContext,
12
+ type AppLayoutDrawerHandle,
13
+ } from './app-layout-context'
8
14
  import { MobileBurgerMenu, MobileBurgerMenuProps } from './mobile-burger-menu'
9
15
  import { NavigationSidebar } from './navigation-sidebar'
10
16
 
11
- /**
12
- * Container element that wraps `<main>` and serves as the portal target for
13
- * `AppLayoutDrawer`. Drawers rendered into this container sit on top of the
14
- * main content area only — the sidebar and header remain visible and
15
- * interactive. Null when AppLayout hasn't mounted (or when used outside of it).
16
- */
17
- const AppLayoutDrawerContainerContext = createContext<HTMLElement | null>(null)
18
-
19
- export function useAppLayoutDrawerContainer(): HTMLElement | null {
20
- return useContext(AppLayoutDrawerContainerContext)
21
- }
22
-
23
- /**
24
- * Two-way coordination between AppLayout's mobile burger menu and in-layout
25
- * drawers (`AppLayoutDrawer`), which render above the menu (z-[103] vs
26
- * z-[101]) — the two must never be open at the same time:
27
- * - a drawer that opens calls `notifyDrawerDidOpen` so the layout closes
28
- * the menu (otherwise it would resurface once the drawer closes);
29
- * - each drawer registers a close handle so opening the menu via the
30
- * burger button closes any open drawer instead of hiding the menu
31
- * underneath it. Null outside of AppLayout.
32
- */
33
- export interface AppLayoutDrawerHandle {
34
- close: () => void
35
- }
36
-
37
- interface AppLayoutDrawerCoordination {
38
- notifyDrawerDidOpen: () => void
39
- /** Returns an unregister cleanup. */
40
- registerDrawer: (handle: AppLayoutDrawerHandle) => () => void
41
- }
42
-
43
- const AppLayoutDrawerCoordinationContext = createContext<AppLayoutDrawerCoordination | null>(null)
44
-
45
- export function useAppLayoutDrawerCoordination(): AppLayoutDrawerCoordination | null {
46
- return useContext(AppLayoutDrawerCoordinationContext)
47
- }
17
+ export { useAppLayoutDrawerContainer, useAppLayoutDrawerCoordination } from './app-layout-context'
18
+ export type { AppLayoutDrawerHandle } from './app-layout-context'
48
19
 
49
20
  export interface AppLayoutProps {
50
21
  children: React.ReactNode
@@ -113,7 +84,14 @@ export function AppLayout({
113
84
  }, [])
114
85
 
115
86
  const drawerCoordination = useMemo<AppLayoutDrawerCoordination>(() => ({
116
- notifyDrawerDidOpen: () => setMobileMenuOpen(false),
87
+ notifyDrawerDidOpen: (self) => {
88
+ setMobileMenuOpen(false)
89
+ // Only one in-layout panel may be open at a time — each dims the main
90
+ // area, so stacking them reads as broken.
91
+ for (const handle of drawerHandlesRef.current) {
92
+ if (handle !== self) handle.close()
93
+ }
94
+ },
117
95
  registerDrawer: (handle) => {
118
96
  drawerHandlesRef.current.add(handle)
119
97
  return () => drawerHandlesRef.current.delete(handle)