@alfakit/react 0.1.11 → 0.1.13

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 (64) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/entries/other-entries.d.ts.map +1 -1
  3. package/dist/components/entries/other-entries.js +1 -1
  4. package/dist/components/entries/other-entries.js.map +1 -1
  5. package/dist/components/fields/builder.d.ts.map +1 -1
  6. package/dist/components/fields/builder.js +3 -3
  7. package/dist/components/fields/builder.js.map +1 -1
  8. package/dist/components/fields/file-upload.d.ts.map +1 -1
  9. package/dist/components/fields/file-upload.js +2 -5
  10. package/dist/components/fields/file-upload.js.map +1 -1
  11. package/dist/components/fields/repeater.d.ts.map +1 -1
  12. package/dist/components/fields/repeater.js +3 -3
  13. package/dist/components/fields/repeater.js.map +1 -1
  14. package/dist/components/fields/rich-editor-extensions.js +5 -2
  15. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  16. package/dist/components/layout/wizard.d.ts.map +1 -1
  17. package/dist/components/layout/wizard.js +4 -1
  18. package/dist/components/layout/wizard.js.map +1 -1
  19. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  20. package/dist/components/notifications/flash-toasts.js +11 -1
  21. package/dist/components/notifications/flash-toasts.js.map +1 -1
  22. package/dist/components/notifications/notifications-menu.js +3 -3
  23. package/dist/components/notifications/notifications-menu.js.map +1 -1
  24. package/dist/components/panel/pwa.d.ts.map +1 -1
  25. package/dist/components/panel/pwa.js +8 -7
  26. package/dist/components/panel/pwa.js.map +1 -1
  27. package/dist/components/panel/shortcuts-overlay.js +1 -1
  28. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  29. package/dist/components/table/cells.js +1 -1
  30. package/dist/components/table/cells.js.map +1 -1
  31. package/dist/components.css +19 -0
  32. package/dist/create-alfakit.d.ts.map +1 -1
  33. package/dist/create-alfakit.js +3 -1
  34. package/dist/create-alfakit.js.map +1 -1
  35. package/dist/eject-manifest.json +2 -2
  36. package/dist/pages/page.js +1 -1
  37. package/dist/pages/page.js.map +1 -1
  38. package/dist/standalone/alfakit-component.js +2 -1
  39. package/dist/standalone/alfakit-component.js.map +1 -1
  40. package/dist/standalone/alfakit-toaster.js +2 -1
  41. package/dist/standalone/alfakit-toaster.js.map +1 -1
  42. package/dist/standalone/runtime.js +1 -11
  43. package/dist/standalone/runtime.js.map +1 -1
  44. package/dist/standalone/toaster-claims.js +16 -0
  45. package/dist/standalone/toaster-claims.js.map +1 -0
  46. package/dist/starter-kit/widgets-loaded.js +63 -6
  47. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/components/entries/other-entries.tsx +2 -0
  50. package/src/components/fields/builder.tsx +7 -2
  51. package/src/components/fields/file-upload.tsx +2 -7
  52. package/src/components/fields/repeater.tsx +6 -3
  53. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  54. package/src/components/layout/wizard.tsx +5 -1
  55. package/src/components/notifications/flash-toasts.tsx +8 -0
  56. package/src/components/notifications/notifications-menu.tsx +3 -3
  57. package/src/components/panel/pwa.tsx +23 -9
  58. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  59. package/src/components/table/cells.tsx +3 -1
  60. package/src/create-alfakit.tsx +6 -0
  61. package/src/pages/page.tsx +1 -1
  62. package/src/standalone/runtime.ts +2 -14
  63. package/src/standalone/toaster-claims.ts +16 -0
  64. package/src/starter-kit/widgets-loaded.tsx +84 -1
@@ -14,6 +14,7 @@ import { toast } from 'sonner'
14
14
  import { usePanel, useT } from '../../context'
15
15
  import { useStore } from '../../hooks/use-store'
16
16
  import { Icon } from '../../lib/icons'
17
+ import { toastOptions } from '../notifications/flash-toasts'
17
18
  import { DropdownMenuItem } from '../ui/dropdown-menu'
18
19
 
19
20
  const UPDATE_TOAST_ID = 'alfakit-pwa-update'
@@ -27,7 +28,7 @@ export async function installPwa(t: Translator['t']): Promise<void> {
27
28
  {},
28
29
  'To install this app, tap the Share button and then “Add to Home Screen”.',
29
30
  ),
30
- { duration: 10000 },
31
+ toastOptions({ duration: 10000 }),
31
32
  )
32
33
  }
33
34
  }
@@ -49,11 +50,14 @@ export function PwaManager() {
49
50
  useEffect(() => {
50
51
  if (!updateAvailable) return
51
52
 
52
- toast(t('pwa.update_available', {}, 'A new version is available'), {
53
- id: UPDATE_TOAST_ID,
54
- duration: Infinity,
55
- action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
56
- })
53
+ toast(
54
+ t('pwa.update_available', {}, 'A new version is available'),
55
+ toastOptions({
56
+ id: UPDATE_TOAST_ID,
57
+ duration: Infinity,
58
+ action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
59
+ }),
60
+ )
57
61
  }, [updateAvailable, t])
58
62
 
59
63
  return null
@@ -74,15 +78,25 @@ export function PwaMenuItems() {
74
78
  state.push === 'subscribed' ? await unsubscribeFromPush() : await subscribeToPush()
75
79
 
76
80
  if (outcome === 'subscribed') {
77
- toast.success(t('pwa.notifications.enabled', {}, 'Notifications are on for this device'))
81
+ toast.success(
82
+ t('pwa.notifications.enabled', {}, 'Notifications are on for this device'),
83
+ toastOptions(),
84
+ )
78
85
  } else if (outcome === 'unsubscribed') {
79
- toast.success(t('pwa.notifications.disabled', {}, 'Notifications are off for this device'))
86
+ toast.success(
87
+ t('pwa.notifications.disabled', {}, 'Notifications are off for this device'),
88
+ toastOptions(),
89
+ )
80
90
  } else if (outcome === 'denied') {
81
91
  toast.error(
82
92
  t('pwa.notifications.blocked', {}, 'Notifications are blocked in your browser settings'),
93
+ toastOptions(),
83
94
  )
84
95
  } else {
85
- toast.error(t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'))
96
+ toast.error(
97
+ t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'),
98
+ toastOptions(),
99
+ )
86
100
  }
87
101
  }
88
102
 
@@ -221,7 +221,7 @@ export function ShortcutsOverlay({
221
221
  aria-label={t(
222
222
  'shortcuts.reset_label',
223
223
  { label: item.label },
224
- `Restore the default shortcut for ${item.label}`,
224
+ `Reset the shortcut for ${item.label}`,
225
225
  )}
226
226
  onClick={() => void save(item.id, null)}
227
227
  >
@@ -242,7 +242,9 @@ function ImageCell({ column, cell }: CellProps) {
242
242
  <span
243
243
  data-slot="image-remaining"
244
244
  className={cn(
245
- 'text-muted-foreground ms-1',
245
+ 'text-muted-foreground',
246
+ // The last image keeps the stack's negative end margin (W-S3-03).
247
+ schema.stacked && usesDefaultImageStack(schema) ? 'ms-3' : 'ms-1',
246
248
  remainingTextSizeClass(schema.limitedRemainingTextSize),
247
249
  )}
248
250
  >
@@ -32,6 +32,7 @@ import { Direction } from 'radix-ui'
32
32
  import {
33
33
  StrictMode,
34
34
  createElement,
35
+ useEffect,
35
36
  useLayoutEffect,
36
37
  type ReactElement,
37
38
  type ReactNode,
@@ -51,6 +52,7 @@ import { discoverComponents } from './lib/discovery'
51
52
  import { preloadIcons } from './lib/icons'
52
53
  import { AlfakitPage } from './pages/page'
53
54
  import { builtInComponents, createDefaultRegistry } from './registry'
55
+ import { claimToaster } from './standalone/toaster-claims'
54
56
 
55
57
  /**
56
58
  * Rendered inside Inertia's page tree, so it follows the current page: the reading
@@ -91,6 +93,10 @@ export function AlfakitRoot({
91
93
  applyPanelTheme(panel)
92
94
  }, [panel])
93
95
 
96
+ // This root renders the toaster: starter kit header components and standalone components
97
+ // on the same page don't mount a second one.
98
+ useEffect(() => claimToaster(), [])
99
+
94
100
  return (
95
101
  <Direction.Provider dir={direction}>
96
102
  <TooltipProvider delayDuration={0}>
@@ -281,7 +281,7 @@ export function ContentBlockRenderer({ block }: { block: ContentBlock }) {
281
281
  <div data-slot="infolist" className="contents">
282
282
  <SchemaRenderer
283
283
  components={(block as InfolistBlock).components}
284
- columns={{ default: 1, lg: 2 }}
284
+ columns={(block as InfolistBlock).columns ?? { default: 1, lg: 2 }}
285
285
  />
286
286
  </div>
287
287
  )
@@ -95,20 +95,8 @@ export function createStandaloneContext(
95
95
 
96
96
  // ---------------------------------------------------------------- Toaster ownership
97
97
 
98
- let toasters = 0
99
-
100
- /** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
101
- export function claimToaster(): () => void {
102
- toasters++
103
-
104
- return () => {
105
- toasters--
106
- }
107
- }
108
-
109
- export function hasToaster(): boolean {
110
- return toasters > 0
111
- }
98
+ // In a module of its own: the starter kit header components check it without loading this one.
99
+ export { claimToaster, hasToaster } from './toaster-claims'
112
100
 
113
101
  // ---------------------------------------------------------------- Flashed notifications
114
102
 
@@ -0,0 +1,16 @@
1
+ // Who renders AlfaKit's toaster on the page: `<AlfakitToaster>`, `AlfakitRoot`, the first
2
+ // standalone component or the starter kit header components on the host's own pages.
3
+ let toasters = 0
4
+
5
+ /** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
6
+ export function claimToaster(): () => void {
7
+ toasters++
8
+
9
+ return () => {
10
+ toasters--
11
+ }
12
+ }
13
+
14
+ export function hasToaster(): boolean {
15
+ return toasters > 0
16
+ }
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  ActionRunner,
3
3
  ComponentRegistry,
4
+ STARTER_KIT_TOASTER_ID,
4
5
  Translator,
5
6
  initials,
6
7
  isAlfakitPage,
7
8
  loadStarterKitPanel,
8
9
  loadedStarterKitPanel,
9
10
  openRequestedStarterKitSearch,
11
+ overlayPlacementDirection,
12
+ resolveToastPosition,
10
13
  showsLanguageSwitcher,
11
14
  starterKitCopilot,
12
15
  starterKitPanelLocale,
@@ -17,12 +20,15 @@ import {
17
20
  tenantNameText,
18
21
  type PanelSchema,
19
22
  type StarterKitWidgets,
23
+ type ToastPosition,
20
24
  } from '@alfakit/core'
21
25
  import { router, usePage } from '@inertiajs/react'
22
26
  import { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'
23
27
  import { Direction } from 'radix-ui'
24
28
  import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
29
+ import { createPortal } from 'react-dom'
25
30
  import { AiCopilot } from '../components/ai/copilot'
31
+ import { setToasterId } from '../components/notifications/flash-toasts'
26
32
  import { NotificationsMenu } from '../components/notifications/notifications-menu'
27
33
  import { RealtimeNotifications } from '../components/notifications/realtime-notifications'
28
34
  import { GlobalSearch } from '../components/panel/global-search'
@@ -38,16 +44,20 @@ import {
38
44
  DropdownMenuSeparator,
39
45
  DropdownMenuTrigger,
40
46
  } from '../components/ui/dropdown-menu'
47
+ import { Toaster } from '../components/ui/sonner'
41
48
  import { TooltipProvider } from '../components/ui/tooltip'
42
49
  import {
43
50
  AlfakitProvider,
44
51
  StandaloneProvider,
45
52
  useLocalization,
53
+ useRegistry,
46
54
  useT,
47
55
  type AlfakitContextValue,
48
56
  type AnyComponent,
49
57
  } from '../context'
50
58
  import { Icon } from '../lib/icons'
59
+ import { ensureFlashToasts } from '../standalone/runtime'
60
+ import { claimToaster, hasToaster } from '../standalone/toaster-claims'
51
61
  import {
52
62
  CopilotTrigger,
53
63
  LanguageSwitcherTrigger,
@@ -117,16 +127,89 @@ function useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {
117
127
  )
118
128
  }
119
129
 
130
+ /**
131
+ * AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):
132
+ * Alfakit's toasts go to it, and so do flashed notifications.
133
+ */
134
+ function HostToaster({ position, dir }: { position?: ToastPosition; dir: 'ltr' | 'rtl' }) {
135
+ const registry = useRegistry()
136
+
137
+ useEffect(() => {
138
+ ensureFlashToasts(registry)
139
+ }, [registry])
140
+
141
+ useEffect(() => {
142
+ setToasterId(STARTER_KIT_TOASTER_ID)
143
+
144
+ return () => setToasterId(undefined)
145
+ }, [])
146
+
147
+ return (
148
+ <Toaster
149
+ id={STARTER_KIT_TOASTER_ID}
150
+ position={resolveToastPosition(position, dir)}
151
+ dir={dir}
152
+ richColors
153
+ closeButton
154
+ />
155
+ )
156
+ }
157
+
158
+ /**
159
+ * On the host's own pages no AlfaKit toaster is mounted, so the first loaded header
160
+ * component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).
161
+ * AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check
162
+ * runs after every component's effects, and again after each visit.
163
+ */
164
+ function useOwnsHostToaster(): boolean {
165
+ const alfakitPage = isAlfakitPage(usePage().props)
166
+ const [owns, setOwns] = useState(false)
167
+
168
+ useEffect(() => {
169
+ if (alfakitPage) return undefined
170
+
171
+ let release: (() => void) | null = null
172
+ let cancelled = false
173
+
174
+ queueMicrotask(() => {
175
+ if (cancelled || hasToaster()) return
176
+
177
+ release = claimToaster()
178
+ setOwns(true)
179
+ })
180
+
181
+ return () => {
182
+ cancelled = true
183
+ release?.()
184
+ setOwns(false)
185
+ }
186
+ }, [alfakitPage])
187
+
188
+ return owns && !alfakitPage
189
+ }
190
+
120
191
  function WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {
121
192
  const context = useWidgetsContext(panel)
122
193
  const page = useMemo(() => starterKitWidgetsPage(), [])
194
+ const ownsToaster = useOwnsHostToaster()
195
+ const direction = panel.localization?.direction ?? 'ltr'
123
196
 
124
197
  return (
125
198
  <AlfakitProvider value={context}>
126
199
  <StandaloneProvider value={{ panel, page }}>
127
- <Direction.Provider dir={panel.localization?.direction ?? 'ltr'}>
200
+ <Direction.Provider dir={direction}>
128
201
  <TooltipProvider delayDuration={0}>{children}</TooltipProvider>
129
202
  </Direction.Provider>
203
+ {/* In the body, not in the header (a header with a backdrop filter would hold its
204
+ fixed position), placed in the document's direction as on AlfaKit pages. */}
205
+ {ownsToaster &&
206
+ createPortal(
207
+ <HostToaster
208
+ position={panel.toastPosition}
209
+ dir={overlayPlacementDirection(direction, true)}
210
+ />,
211
+ document.body,
212
+ )}
130
213
  </StandaloneProvider>
131
214
  </AlfakitProvider>
132
215
  )