@alfakit/vue 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 (93) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/AlfakitRoot.vue.d.ts.map +1 -1
  3. package/dist/AlfakitRoot.vue_vue_type_script_setup_true_lang.js +47 -40
  4. package/dist/AlfakitRoot.vue_vue_type_script_setup_true_lang.js.map +1 -1
  5. package/dist/components/entries/ImageEntryComponent.vue.d.ts.map +1 -1
  6. package/dist/components/entries/ImageEntryComponent.vue_vue_type_script_setup_true_lang.js +1 -1
  7. package/dist/components/entries/ImageEntryComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  8. package/dist/components/fields/BuilderField.vue.d.ts.map +1 -1
  9. package/dist/components/fields/BuilderField.vue_vue_type_script_setup_true_lang.js +114 -114
  10. package/dist/components/fields/BuilderField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  11. package/dist/components/fields/FileUploadField.vue.d.ts.map +1 -1
  12. package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js +143 -146
  13. package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  14. package/dist/components/fields/RepeaterField.vue.d.ts.map +1 -1
  15. package/dist/components/fields/RepeaterField.vue_vue_type_script_setup_true_lang.js +50 -50
  16. package/dist/components/fields/RepeaterField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/fields/rich-editor-extensions.d.ts.map +1 -1
  18. package/dist/components/fields/rich-editor-extensions.js +4 -2
  19. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  20. package/dist/components/layout/WizardComponent.vue.d.ts.map +1 -1
  21. package/dist/components/layout/WizardComponent.vue_vue_type_script_setup_true_lang.js +41 -40
  22. package/dist/components/layout/WizardComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  23. package/dist/components/notifications/NotificationsMenu.vue.d.ts.map +1 -1
  24. package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js +103 -102
  25. package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  26. package/dist/components/notifications/flash-toasts.d.ts +7 -0
  27. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  28. package/dist/components/notifications/flash-toasts.js +12 -6
  29. package/dist/components/notifications/flash-toasts.js.map +1 -1
  30. package/dist/components/panel/PwaManager.vue.d.ts.map +1 -1
  31. package/dist/components/panel/PwaManager.vue_vue_type_script_setup_true_lang.js +16 -15
  32. package/dist/components/panel/PwaManager.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/panel/PwaMenuItems.vue.d.ts.map +1 -1
  34. package/dist/components/panel/PwaMenuItems.vue_vue_type_script_setup_true_lang.js +27 -26
  35. package/dist/components/panel/PwaMenuItems.vue_vue_type_script_setup_true_lang.js.map +1 -1
  36. package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/panel/pwa.d.ts.map +1 -1
  39. package/dist/components/panel/pwa.js +6 -5
  40. package/dist/components/panel/pwa.js.map +1 -1
  41. package/dist/components/table/TableCellContent.vue.d.ts.map +1 -1
  42. package/dist/components/table/TableCellContent.vue_vue_type_script_setup_true_lang.js +41 -41
  43. package/dist/components/table/TableCellContent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  44. package/dist/components.css +19 -0
  45. package/dist/eject-manifest.json +2 -2
  46. package/dist/lib/discovery.d.ts.map +1 -1
  47. package/dist/lib/discovery.js +15 -14
  48. package/dist/lib/discovery.js.map +1 -1
  49. package/dist/lib/hydration.d.ts +15 -0
  50. package/dist/lib/hydration.d.ts.map +1 -1
  51. package/dist/lib/hydration.js +22 -8
  52. package/dist/lib/hydration.js.map +1 -1
  53. package/dist/pages/ContentBlockRenderer.vue_vue_type_script_setup_true_lang.js +2 -2
  54. package/dist/pages/ContentBlockRenderer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  55. package/dist/standalone/AlfakitComponent.vue.d.ts.map +1 -1
  56. package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js +34 -30
  57. package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  58. package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js +2 -1
  59. package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/standalone/runtime.d.ts +1 -3
  61. package/dist/standalone/runtime.d.ts.map +1 -1
  62. package/dist/standalone/runtime.js +26 -34
  63. package/dist/standalone/runtime.js.map +1 -1
  64. package/dist/standalone/toaster-claims.d.ts +4 -0
  65. package/dist/standalone/toaster-claims.d.ts.map +1 -0
  66. package/dist/standalone/toaster-claims.js +14 -0
  67. package/dist/standalone/toaster-claims.js.map +1 -0
  68. package/dist/starter-kit/widgets/WidgetShell.vue.d.ts +2 -2
  69. package/dist/starter-kit/widgets/WidgetShell.vue.d.ts.map +1 -1
  70. package/dist/starter-kit/widgets/WidgetShell.vue_vue_type_script_setup_true_lang.js +50 -18
  71. package/dist/starter-kit/widgets/WidgetShell.vue_vue_type_script_setup_true_lang.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/AlfakitRoot.vue +16 -1
  74. package/src/components/entries/ImageEntryComponent.vue +2 -0
  75. package/src/components/fields/BuilderField.vue +7 -2
  76. package/src/components/fields/FileUploadField.vue +2 -7
  77. package/src/components/fields/RepeaterField.vue +9 -2
  78. package/src/components/fields/rich-editor-extensions.ts +9 -5
  79. package/src/components/layout/WizardComponent.vue +5 -1
  80. package/src/components/notifications/NotificationsMenu.vue +3 -2
  81. package/src/components/notifications/flash-toasts.ts +8 -0
  82. package/src/components/panel/PwaManager.vue +9 -5
  83. package/src/components/panel/PwaMenuItems.vue +14 -3
  84. package/src/components/panel/ShortcutsOverlay.vue +1 -1
  85. package/src/components/panel/pwa.ts +2 -1
  86. package/src/components/table/TableCellContent.vue +3 -1
  87. package/src/lib/discovery.ts +6 -0
  88. package/src/lib/hydration.ts +38 -2
  89. package/src/pages/ContentBlockRenderer.vue +1 -1
  90. package/src/standalone/AlfakitComponent.vue +4 -0
  91. package/src/standalone/runtime.ts +2 -14
  92. package/src/standalone/toaster-claims.ts +16 -0
  93. package/src/starter-kit/widgets/WidgetShell.vue +77 -2
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { RepeaterSchema, SchemaComponent } from '@alfakit/core'
3
3
  import {
4
+ itemExtraActions,
4
5
  isDeferredPlaceholder,
5
6
  VIRTUAL_ESTIMATES,
6
7
  appendRepeaterItem,
@@ -194,7 +195,9 @@ const add = () =>
194
195
  :virtual="virtual"
195
196
  :context="context"
196
197
  :resolve="resolve"
197
- :extra-actions-for="(index: number) => props.component.items[index]?.actions"
198
+ :extra-actions-for="
199
+ (index: number) => itemExtraActions(props.component, props.component.items[index])
200
+ "
198
201
  @move="moveItem"
199
202
  @clone="cloneItem"
200
203
  @remove="removeItem"
@@ -272,7 +275,11 @@ const add = () =>
272
275
  :index="index"
273
276
  :count="items.length"
274
277
  :resolve="(name: ItemActionName) => resolve(name, index)"
275
- :extra-actions="disabled ? undefined : props.component.items[index]?.actions"
278
+ :extra-actions="
279
+ disabled
280
+ ? undefined
281
+ : itemExtraActions(props.component, props.component.items[index])
282
+ "
276
283
  :buttons="buttons"
277
284
  :can-clone="Boolean(props.component.cloneable) && canAdd"
278
285
  :can-delete="canDelete"
@@ -102,11 +102,15 @@ export function useRichEditorExtensions(
102
102
 
103
103
  if (!result || !instance || instance.isDestroyed) return
104
104
 
105
- instance
106
- .chain()
107
- .focus()
108
- .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })
109
- .run()
105
+ const block = { type: 'customBlock', attrs: { id: result.id, config: result.config } }
106
+ // A block inserted just before is still selected (a node selection): insert after it
107
+ // instead of replacing it (W-S2-05).
108
+ const { selection } = instance.state
109
+ const chain = instance.chain().focus()
110
+ ;('node' in selection && selection.node
111
+ ? chain.insertContentAt(selection.to, block)
112
+ : chain.insertContent(block)
113
+ ).run()
110
114
  }
111
115
 
112
116
  return { extensions, suggestions, insertBlock, context }
@@ -21,6 +21,7 @@ import { cn } from '../../lib/utils'
21
21
  import ActionButton from '../actions/ActionButton.vue'
22
22
  import { usePageActionContext } from '../actions/action-context'
23
23
  import { useOptionalFormStore } from '../form/form-context'
24
+ import { useFieldActionContext } from '../schema/field-actions'
24
25
  import SchemaRenderer from '../schema/SchemaRenderer.vue'
25
26
  import Button from '../ui/Button.vue'
26
27
  import DeferredSchema from './DeferredSchema.vue'
@@ -52,6 +53,9 @@ watch(
52
53
  { immediate: true },
53
54
  )
54
55
  const store = useOptionalFormStore()
56
+ // Inside a form the submit action runs as a field action, so `$get` / `$set` work (W-S2-09).
57
+ const fieldContext = useFieldActionContext(() => props.component)
58
+ const submitContext = computed(() => (store ? fieldContext.value : actionContext))
55
59
 
56
60
  // An action modal around this wizard shows its submit button on the last step only.
57
61
  watch(
@@ -234,7 +238,7 @@ const next = async () => {
234
238
  <ActionButton
235
239
  v-else-if="props.component.submitAction"
236
240
  :action="props.component.submitAction"
237
- :context="actionContext"
241
+ :context="submitContext"
238
242
  />
239
243
  <div
240
244
  v-else-if="props.component.submitActionHtml"
@@ -11,6 +11,7 @@ import {
11
11
  import { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from '@lucide/vue'
12
12
  import { computed, onMounted, onScopeDispose, ref, watch } from 'vue'
13
13
  import { toast } from 'vue-sonner'
14
+ import { toastOptions } from './flash-toasts'
14
15
  import { useStore } from '../../composables/use-store'
15
16
  import { useDirection, useFormatter, usePanel, useT } from '../../context'
16
17
  import { cssColor, textColorClasses } from '../../lib/colors'
@@ -113,8 +114,8 @@ async function togglePush() {
113
114
  const { message } = await togglePushNotifications()
114
115
  const text = t(message.key, {}, message.fallback)
115
116
 
116
- if (message.error) toast.error(text)
117
- else toast.success(text)
117
+ if (message.error) toast.error(text, toastOptions())
118
+ else toast.success(text, toastOptions())
118
119
  }
119
120
  </script>
120
121
 
@@ -18,6 +18,14 @@ export function setToasterId(id: string | undefined): void {
18
18
  toasterId = id
19
19
  }
20
20
 
21
+ /**
22
+ * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and
23
+ * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).
24
+ */
25
+ export function toastOptions<T extends object>(options?: T): T & { toasterId?: string } {
26
+ return { ...(options ?? ({} as T)), toasterId }
27
+ }
28
+
21
29
  /** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */
22
30
  export function listenForClosedNotifications(): void {
23
31
  if (closeListening || typeof window === 'undefined') return
@@ -2,6 +2,7 @@
2
2
  import { applyPwaUpdate, pwaStore, startPwa } from '@alfakit/core'
3
3
  import { onMounted, watch } from 'vue'
4
4
  import { toast } from 'vue-sonner'
5
+ import { toastOptions } from '../notifications/flash-toasts'
5
6
  import { useStore } from '../../composables/use-store'
6
7
  import { usePanel, useT } from '../../context'
7
8
 
@@ -26,11 +27,14 @@ watch(
26
27
  (available) => {
27
28
  if (!available) return
28
29
 
29
- toast(t('pwa.update_available', {}, 'A new version is available'), {
30
- id: 'alfakit-pwa-update',
31
- duration: Infinity,
32
- action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
33
- })
30
+ toast(
31
+ t('pwa.update_available', {}, 'A new version is available'),
32
+ toastOptions({
33
+ id: 'alfakit-pwa-update',
34
+ duration: Infinity,
35
+ action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
36
+ }),
37
+ )
34
38
  },
35
39
  { immediate: true },
36
40
  )
@@ -3,6 +3,7 @@ import { canInstallPwa, pwaStore, subscribeToPush, unsubscribeFromPush } from '@
3
3
  import { BellIcon, BellOffIcon, DownloadIcon } from '@lucide/vue'
4
4
  import { computed } from 'vue'
5
5
  import { toast } from 'vue-sonner'
6
+ import { toastOptions } from '../notifications/flash-toasts'
6
7
  import { useStore } from '../../composables/use-store'
7
8
  import { usePanel, useT } from '../../context'
8
9
  import { Icon } from '../../lib/icons'
@@ -23,15 +24,25 @@ async function togglePush() {
23
24
  state.value.push === 'subscribed' ? await unsubscribeFromPush() : await subscribeToPush()
24
25
 
25
26
  if (outcome === 'subscribed') {
26
- toast.success(t('pwa.notifications.enabled', {}, 'Notifications are on for this device'))
27
+ toast.success(
28
+ t('pwa.notifications.enabled', {}, 'Notifications are on for this device'),
29
+ toastOptions(),
30
+ )
27
31
  } else if (outcome === 'unsubscribed') {
28
- toast.success(t('pwa.notifications.disabled', {}, 'Notifications are off for this device'))
32
+ toast.success(
33
+ t('pwa.notifications.disabled', {}, 'Notifications are off for this device'),
34
+ toastOptions(),
35
+ )
29
36
  } else if (outcome === 'denied') {
30
37
  toast.error(
31
38
  t('pwa.notifications.blocked', {}, 'Notifications are blocked in your browser settings'),
39
+ toastOptions(),
32
40
  )
33
41
  } else {
34
- toast.error(t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'))
42
+ toast.error(
43
+ t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'),
44
+ toastOptions(),
45
+ )
35
46
  }
36
47
  }
37
48
  </script>
@@ -216,7 +216,7 @@ const update = (next: boolean) => {
216
216
  t(
217
217
  'shortcuts.reset_label',
218
218
  { label: item.label },
219
- `Restore the default shortcut for ${item.label}`,
219
+ `Reset the shortcut for ${item.label}`,
220
220
  )
221
221
  "
222
222
  @click="save(item.id, null)"
@@ -1,5 +1,6 @@
1
1
  import { promptPwaInstall, type Translator } from '@alfakit/core'
2
2
  import { toast } from 'vue-sonner'
3
+ import { toastOptions } from '../notifications/flash-toasts'
3
4
 
4
5
  /** Show the browser's install prompt, or the "Add to Home Screen" hint on iOS Safari. */
5
6
  export async function installPwa(t: Translator['t']): Promise<void> {
@@ -10,7 +11,7 @@ export async function installPwa(t: Translator['t']): Promise<void> {
10
11
  {},
11
12
  'To install this app, tap the Share button and then “Add to Home Screen”.',
12
13
  ),
13
- { duration: 10000 },
14
+ toastOptions({ duration: 10000 }),
14
15
  )
15
16
  }
16
17
  }
@@ -191,7 +191,9 @@ const descriptionPosition = computed(
191
191
  data-slot="image-remaining"
192
192
  :class="
193
193
  cn(
194
- 'text-muted-foreground ms-1',
194
+ 'text-muted-foreground',
195
+ // The last image keeps the stack's negative end margin (W-S3-03).
196
+ imageSchema.stacked && defaultImageStack ? 'ms-3' : 'ms-1',
195
197
  remainingTextSizeClass(imageSchema.limitedRemainingTextSize),
196
198
  )
197
199
  "
@@ -6,6 +6,7 @@ import {
6
6
  } from '@alfakit/core'
7
7
  import { defineAsyncComponent, defineComponent, h } from 'vue'
8
8
  import type { AnyComponent } from '../context'
9
+ import { startIdScope } from './hydration'
9
10
 
10
11
  /**
11
12
  * A discovered component: rendered straight away once its code has loaded (the pages that
@@ -21,6 +22,11 @@ export function discoveredComponent(entry: DiscoveredEntry<AnyComponent>): AnyCo
21
22
  // Chosen once per mount, so loading never remounts a field.
22
23
  const Loaded = entry.current ?? Async
23
24
 
25
+ // The async component starts an id scope; a loaded one starts the same, so the server
26
+ // (which keeps loaded code between requests) and a hydrating browser that still loads
27
+ // it number Reka's ids alike (W-V2-13, W-V3-28).
28
+ if (Loaded !== Async) startIdScope()
29
+
24
30
  return () => h(Loaded, attrs, slots)
25
31
  },
26
32
  })
@@ -12,12 +12,17 @@ const mountedApps = new WeakMap<AppContext, Ref<boolean>>()
12
12
  * the server render and hydration), then `true` for every component in that app, so
13
13
  * content rendered later draws at once (reactive). Lazily loaded parts (named icons, heavy
14
14
  * fields) render their server markup until then, whatever has loaded already.
15
+ *
16
+ * A component that hydrates server markup after the app mounted (inside an async
17
+ * component whose code arrived later, such as a lazily split built-in) waits for its own
18
+ * mount: its server markup is the fallback too (W-V3-28).
15
19
  */
16
20
  export function useAppMounted(): Ref<boolean> {
17
- const context = getCurrentInstance()?.appContext
21
+ const instance = getCurrentInstance()
22
+ const context = instance?.appContext
18
23
 
19
24
  // Outside a component there is no hydration to wait for.
20
- if (!context) return ref(true)
25
+ if (!instance || !context) return ref(true)
21
26
 
22
27
  let mounted = mountedApps.get(context)
23
28
 
@@ -27,9 +32,40 @@ export function useAppMounted(): Ref<boolean> {
27
32
  }
28
33
 
29
34
  const flag = mounted
35
+
36
+ // Hydration hands each component its server element before setup runs.
37
+ if (instance.vnode.el && flag.value) {
38
+ const own = ref(false)
39
+
40
+ onMounted(() => {
41
+ own.value = true
42
+ })
43
+
44
+ return own
45
+ }
46
+
30
47
  onMounted(() => {
31
48
  flag.value = true
32
49
  })
33
50
 
34
51
  return flag
35
52
  }
53
+
54
+ /**
55
+ * Start a new `useId()` scope at the current component, as Vue does at an async component
56
+ * (its internal `markAsyncBoundary()`): ids below it are numbered from zero under a prefix
57
+ * that counts scopes, not the ids generated before it. So
58
+ * - AlfaKit's ids don't follow the host layout's (a starter kit's sidebar renders a sheet on
59
+ * phones in the browser but the desktop sidebar on the server), and
60
+ * - a lazily split component renders the same ids whether its code was loaded before
61
+ * rendering (the server after its first request) or arrives during hydration (an async
62
+ * component, which starts a scope itself).
63
+ */
64
+ export function startIdScope(): void {
65
+ const instance = getCurrentInstance() as { ids?: [string, number, number] } | null
66
+ const ids = instance?.ids
67
+
68
+ if (!instance || !Array.isArray(ids) || ids.length !== 3) return
69
+
70
+ instance.ids = [`${ids[0]}${ids[2]++}-`, 0, 0]
71
+ }
@@ -38,7 +38,7 @@ const mainTable = computed(() => props.block.type === 'Table' && props.block.key
38
38
  <div v-else-if="props.block.type === 'Infolist'" data-slot="infolist" class="contents">
39
39
  <SchemaRenderer
40
40
  :components="(props.block as InfolistBlock).components"
41
- :columns="{ default: 1, lg: 2 }"
41
+ :columns="(props.block as InfolistBlock).columns ?? { default: 1, lg: 2 }"
42
42
  />
43
43
  </div>
44
44
  <SchemaRenderer
@@ -21,6 +21,7 @@ import {
21
21
  type AlfakitContextValue,
22
22
  type AnyComponent,
23
23
  } from '../context'
24
+ import { startIdScope } from '../lib/hydration'
24
25
  import { cn } from '../lib/utils'
25
26
  import ContentBlockRenderer from '../pages/ContentBlockRenderer.vue'
26
27
  import AlfakitToaster from './AlfakitToaster.vue'
@@ -59,6 +60,9 @@ defineSlots<{
59
60
  default?: (props: { blocks: ContentBlock[]; page: StandaloneComponentSchema }) => unknown
60
61
  }>()
61
62
 
63
+ // Reka's ids inside the component don't depend on the host page's (W-V2-13).
64
+ startIdScope()
65
+
62
66
  const parent = useOptionalAlfakit()
63
67
  const inertiaPage = usePage()
64
68
 
@@ -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,9 +1,21 @@
1
1
  <script setup lang="ts">
2
- import { starterKitWidgetsPage, type PanelSchema } from '@alfakit/core'
2
+ import {
3
+ isAlfakitPage,
4
+ overlayPlacementDirection,
5
+ resolveToastPosition,
6
+ STARTER_KIT_TOASTER_ID,
7
+ starterKitWidgetsPage,
8
+ type PanelSchema,
9
+ } from '@alfakit/core'
10
+ import { usePage } from '@inertiajs/vue3'
3
11
  import { ConfigProvider } from 'reka-ui'
4
- import { computed, toRef, watch } from 'vue'
12
+ import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
13
+ import { setToasterId } from '../../components/notifications/flash-toasts'
14
+ import Toaster from '../../components/ui/Toaster.vue'
5
15
  import TooltipProvider from '../../components/ui/TooltipProvider.vue'
6
16
  import { provideAlfakit, provideStandalone } from '../../context'
17
+ import { ensureFlashToasts } from '../../standalone/runtime'
18
+ import { claimToaster, hasToaster } from '../../standalone/toaster-claims'
7
19
  import { widgetsAlfakitContext } from './use-widgets'
8
20
 
9
21
  /**
@@ -26,6 +38,58 @@ watch(
26
38
  },
27
39
  { immediate: true, flush: 'sync' },
28
40
  )
41
+
42
+ /**
43
+ * On the host's own pages no AlfaKit toaster is mounted, so the first loaded header
44
+ * component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).
45
+ * AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check
46
+ * runs after every component mounted, and again after each visit.
47
+ */
48
+ const inertiaPage = usePage()
49
+ const alfakitPage = computed(() => isAlfakitPage(inertiaPage.props))
50
+ const ownsToaster = ref(false)
51
+ const placement = computed(() =>
52
+ overlayPlacementDirection(props.panel.localization?.direction ?? 'ltr', true),
53
+ )
54
+ let releaseToaster: (() => void) | null = null
55
+ let check = 0
56
+
57
+ function releaseHostToaster(): void {
58
+ check++
59
+
60
+ if (releaseToaster) {
61
+ releaseToaster()
62
+ setToasterId(undefined)
63
+ }
64
+
65
+ releaseToaster = null
66
+ ownsToaster.value = false
67
+ }
68
+
69
+ function claimHostToaster(): void {
70
+ releaseHostToaster()
71
+
72
+ if (alfakitPage.value) return
73
+
74
+ const current = check
75
+
76
+ queueMicrotask(() => {
77
+ if (current !== check || hasToaster()) return
78
+
79
+ releaseToaster = claimToaster()
80
+ ownsToaster.value = true
81
+ // As `<AlfakitToaster>`: Alfakit's toasts and flashed notifications go to this one.
82
+ setToasterId(STARTER_KIT_TOASTER_ID)
83
+ ensureFlashToasts(context.registry)
84
+ })
85
+ }
86
+
87
+ onMounted(() => {
88
+ claimHostToaster()
89
+ watch(alfakitPage, claimHostToaster)
90
+ })
91
+
92
+ onUnmounted(releaseHostToaster)
29
93
  </script>
30
94
 
31
95
  <template>
@@ -34,4 +98,15 @@ watch(
34
98
  <slot />
35
99
  </TooltipProvider>
36
100
  </ConfigProvider>
101
+ <!-- In the body, not in the header (a header with a backdrop filter would hold its fixed
102
+ position), placed in the document's direction as on AlfaKit pages. -->
103
+ <Teleport v-if="ownsToaster" to="body">
104
+ <Toaster
105
+ :id="STARTER_KIT_TOASTER_ID"
106
+ :position="resolveToastPosition(props.panel.toastPosition, placement)"
107
+ :dir="placement"
108
+ rich-colors
109
+ close-button
110
+ />
111
+ </Teleport>
37
112
  </template>