@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.
- package/CHANGELOG.md +12 -0
- package/dist/AlfakitRoot.vue.d.ts.map +1 -1
- package/dist/AlfakitRoot.vue_vue_type_script_setup_true_lang.js +47 -40
- package/dist/AlfakitRoot.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/entries/ImageEntryComponent.vue.d.ts.map +1 -1
- package/dist/components/entries/ImageEntryComponent.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/entries/ImageEntryComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/BuilderField.vue.d.ts.map +1 -1
- package/dist/components/fields/BuilderField.vue_vue_type_script_setup_true_lang.js +114 -114
- package/dist/components/fields/BuilderField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/FileUploadField.vue.d.ts.map +1 -1
- package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js +143 -146
- package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/RepeaterField.vue.d.ts.map +1 -1
- package/dist/components/fields/RepeaterField.vue_vue_type_script_setup_true_lang.js +50 -50
- package/dist/components/fields/RepeaterField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/rich-editor-extensions.d.ts.map +1 -1
- package/dist/components/fields/rich-editor-extensions.js +4 -2
- package/dist/components/fields/rich-editor-extensions.js.map +1 -1
- package/dist/components/layout/WizardComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/WizardComponent.vue_vue_type_script_setup_true_lang.js +41 -40
- package/dist/components/layout/WizardComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue.d.ts.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js +103 -102
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/notifications/flash-toasts.d.ts +7 -0
- package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
- package/dist/components/notifications/flash-toasts.js +12 -6
- package/dist/components/notifications/flash-toasts.js.map +1 -1
- package/dist/components/panel/PwaManager.vue.d.ts.map +1 -1
- package/dist/components/panel/PwaManager.vue_vue_type_script_setup_true_lang.js +16 -15
- package/dist/components/panel/PwaManager.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/PwaMenuItems.vue.d.ts.map +1 -1
- package/dist/components/panel/PwaMenuItems.vue_vue_type_script_setup_true_lang.js +27 -26
- package/dist/components/panel/PwaMenuItems.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/pwa.d.ts.map +1 -1
- package/dist/components/panel/pwa.js +6 -5
- package/dist/components/panel/pwa.js.map +1 -1
- package/dist/components/table/TableCellContent.vue.d.ts.map +1 -1
- package/dist/components/table/TableCellContent.vue_vue_type_script_setup_true_lang.js +41 -41
- package/dist/components/table/TableCellContent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components.css +19 -0
- package/dist/eject-manifest.json +2 -2
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +15 -14
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/hydration.d.ts +15 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +22 -8
- package/dist/lib/hydration.js.map +1 -1
- package/dist/pages/ContentBlockRenderer.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/pages/ContentBlockRenderer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone/AlfakitComponent.vue.d.ts.map +1 -1
- package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js +34 -30
- package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone/runtime.d.ts +1 -3
- package/dist/standalone/runtime.d.ts.map +1 -1
- package/dist/standalone/runtime.js +26 -34
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone/toaster-claims.d.ts +4 -0
- package/dist/standalone/toaster-claims.d.ts.map +1 -0
- package/dist/standalone/toaster-claims.js +14 -0
- package/dist/standalone/toaster-claims.js.map +1 -0
- package/dist/starter-kit/widgets/WidgetShell.vue.d.ts +2 -2
- package/dist/starter-kit/widgets/WidgetShell.vue.d.ts.map +1 -1
- package/dist/starter-kit/widgets/WidgetShell.vue_vue_type_script_setup_true_lang.js +50 -18
- package/dist/starter-kit/widgets/WidgetShell.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/package.json +2 -2
- package/src/AlfakitRoot.vue +16 -1
- package/src/components/entries/ImageEntryComponent.vue +2 -0
- package/src/components/fields/BuilderField.vue +7 -2
- package/src/components/fields/FileUploadField.vue +2 -7
- package/src/components/fields/RepeaterField.vue +9 -2
- package/src/components/fields/rich-editor-extensions.ts +9 -5
- package/src/components/layout/WizardComponent.vue +5 -1
- package/src/components/notifications/NotificationsMenu.vue +3 -2
- package/src/components/notifications/flash-toasts.ts +8 -0
- package/src/components/panel/PwaManager.vue +9 -5
- package/src/components/panel/PwaMenuItems.vue +14 -3
- package/src/components/panel/ShortcutsOverlay.vue +1 -1
- package/src/components/panel/pwa.ts +2 -1
- package/src/components/table/TableCellContent.vue +3 -1
- package/src/lib/discovery.ts +6 -0
- package/src/lib/hydration.ts +38 -2
- package/src/pages/ContentBlockRenderer.vue +1 -1
- package/src/standalone/AlfakitComponent.vue +4 -0
- package/src/standalone/runtime.ts +2 -14
- package/src/standalone/toaster-claims.ts +16 -0
- 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="
|
|
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="
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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="
|
|
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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
42
|
+
toast.error(
|
|
43
|
+
t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'),
|
|
44
|
+
toastOptions(),
|
|
45
|
+
)
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
48
|
</script>
|
|
@@ -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
|
|
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
|
"
|
package/src/lib/discovery.ts
CHANGED
|
@@ -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
|
})
|
package/src/lib/hydration.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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>
|