feat(layout): re-enable "Back to hub" with a sticky sheet footer #128

Merged
padreug merged 1 commit from feat/back-to-hub-sticky-footer into dev 2026-06-19 22:28:44 +00:00
11 changed files with 183 additions and 149 deletions
Showing only changes of commit 2cc8e34b9d - Show all commits

feat(layout): re-enable "Back to hub" with a sticky sheet footer

Reverses the events-only hide (c037d45) now that the link has a real
home. Three parts:

- Add a @brand-hub-logo alias (brandHubLogoAliasEntry) resolving to the
  brand's primary/global logo — the HUB's logo, never the per-standalone
  @brand-app-logo. Wired into all 9 app vite configs since the shared
  ProfileSheetContent renders it.
- Restructure the profile sheet into a fixed-height flex column: a
  flex-1 min-h-0 overflow-y-auto scroll region over a shrink-0 footer,
  so "Back to hub" + the log-in/out bar stay pinned to the bottom while
  the identity/preferences area scrolls.
- Move the edit-profile Dialog out of the flex root (it portals to body,
  so it's not part of the sheet flow).

Logo bumped to w-8 h-8, centered, with tightened footer padding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Padreug 2026-06-20 00:26:52 +02:00

View file

@ -66,9 +66,7 @@ const npubPreview = computed(() => {
return value.length > 24 ? `${value.slice(0, 12)}${value.slice(-8)}` : value return value.length > 24 ? `${value.slice(0, 12)}${value.slice(-8)}` : value
}) })
// "Back to hub" is hidden for the events-only launch (see template). const hubRootUrl = computed(() => import.meta.env.VITE_HUB_ROOT_URL || '/')
// Re-enable this alongside the commented-out link below when the hub ships.
// const hubRootUrl = computed(() => import.meta.env.VITE_HUB_ROOT_URL || '/')
const copiedField = ref<string | null>(null) const copiedField = ref<string | null>(null)
async function copyToClipboard(text: string, field: string) { async function copyToClipboard(text: string, field: string) {
@ -105,156 +103,166 @@ async function onLogout() {
</script> </script>
<template> <template>
<SheetHeader> <!-- Fill the sheet exactly (h-full) so the footer stays stuck to the
<SheetTitle>{{ t('common.nav.profile') }}</SheetTitle> bottom while only the region above it scrolls. The sheet host already
<SheetDescription v-if="isAuthenticated"> has overflow-y-auto, but with an exact-fit child it never triggers
{{ t('common.nav.profileDescription') }} the inner flex-1 region owns the scroll instead. -->
</SheetDescription> <div class="flex h-full flex-col">
<SheetDescription v-else> <!-- Scrollable region: everything above the pinned footer. min-h-0 lets
{{ t('common.nav.profileLoggedOutDescription') }} this flex child shrink below its content height so it can scroll. -->
</SheetDescription> <div class="flex-1 min-h-0 overflow-y-auto">
</SheetHeader> <SheetHeader>
<SheetTitle>{{ t('common.nav.profile') }}</SheetTitle>
<SheetDescription v-if="isAuthenticated">
{{ t('common.nav.profileDescription') }}
</SheetDescription>
<SheetDescription v-else>
{{ t('common.nav.profileLoggedOutDescription') }}
</SheetDescription>
</SheetHeader>
<!-- Identity card (logged in) summary with an inline edit (pencil) <!-- Identity card (logged in) summary with an inline edit (pencil)
button that opens the profile form. --> button that opens the profile form. -->
<div v-if="isAuthenticated" class="mt-4 rounded-lg border bg-muted/30 p-3 space-y-4"> <div v-if="isAuthenticated" class="mt-4 rounded-lg border bg-muted/30 p-3 space-y-4">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
<Avatar class="h-12 w-12 shrink-0"> <Avatar class="h-12 w-12 shrink-0">
<AvatarImage v-if="pictureUrl" :src="pictureUrl" :alt="displayName ?? ''" /> <AvatarImage v-if="pictureUrl" :src="pictureUrl" :alt="displayName ?? ''" />
<AvatarFallback>{{ fallbackInitial || '?' }}</AvatarFallback> <AvatarFallback>{{ fallbackInitial || '?' }}</AvatarFallback>
</Avatar> </Avatar>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium truncate">{{ displayName || user?.username }}</p> <p class="text-sm font-medium truncate">{{ displayName || user?.username }}</p>
<p v-if="displayName && user?.username" class="text-xs text-muted-foreground truncate"> <p v-if="displayName && user?.username" class="text-xs text-muted-foreground truncate">
@{{ user.username }} @{{ user.username }}
</p> </p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8 shrink-0 self-start text-muted-foreground" class="h-8 w-8 shrink-0 self-start text-muted-foreground"
:aria-label="t('common.nav.editProfile', 'Edit profile')" :aria-label="t('common.nav.editProfile', 'Edit profile')"
@click="editProfileOpen = true" @click="editProfileOpen = true"
>
<Pencil class="h-4 w-4" />
</Button>
</div>
<!-- Identifier rows: full-width value with a corner-offset "legend"
badge straddling the top border (fieldset-legend pattern). The
value gets the entire row so long bech32 / username@domain
strings have room to render. -->
<div class="space-y-3 pt-1">
<!-- Lightning Address this is also the NIP-05 in this stack,
but the @username above already signals the NIP-05. -->
<button
v-if="lightningAddress"
type="button"
class="relative w-full rounded-md border bg-background/60 px-3 pt-3 pb-2 text-left hover:bg-background transition-colors min-w-0"
:aria-label="t('common.nav.copyLightning', 'Copy Lightning address')"
@click="copyToClipboard(lightningAddress, 'lightning')"
>
<span class="absolute -top-2 left-2 inline-flex items-center gap-1 rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground leading-none">
<Zap class="w-3 h-3 text-yellow-500 fill-yellow-500" />
{{ t('common.nav.lightning', 'Lightning') }}
</span>
<span class="block truncate pr-6 text-xs font-mono">{{ lightningAddress }}</span>
<component
:is="copiedField === 'lightning' ? Check : Copy"
class="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground"
/>
</button>
<!-- npub copy the full bech32 even though we display a preview. -->
<button
v-if="npub"
type="button"
class="relative w-full rounded-md border bg-background/60 px-3 pt-3 pb-2 text-left hover:bg-background transition-colors min-w-0"
:aria-label="t('common.nav.copyNpub', 'Copy npub')"
@click="copyToClipboard(npub, 'npub')"
>
<span class="absolute -top-2 left-2 inline-flex items-center rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground leading-none">
{{ t('common.nav.npub', 'npub') }}
</span>
<span class="block truncate pr-6 text-xs font-mono">{{ npubPreview }}</span>
<component
:is="copiedField === 'npub' ? Check : Copy"
class="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground"
/>
</button>
</div>
</div>
<!-- App-specific nav items (rendered by callers like StandaloneMenu) -->
<slot name="app-nav" />
<!-- Cross-app links + global preferences (always visible, auth or not) -->
<div class="mt-4">
<!-- "Back to hub" hidden for the events-only launch re-enable when
the hub ships. When re-enabled, show the HUB's brand-kit logo
(the brand's primary/global logo, or a hub-specific override)
NOT the per-standalone @brand-app-logo, which resolves to this
standalone's own logo. This needs a hub-logo alias (e.g.
@brand-hub-logo = resolveAppLogo for the hub) added in
vite-branding.ts + the standalone vite configs.
<a
:href="hubRootUrl"
class="flex items-center justify-between gap-3 px-3 py-3 hover:bg-accent rounded-md transition-colors"
:aria-label="t('common.nav.backToHub')"
>
<div class="flex items-center gap-3">
<img src="@brand-hub-logo" :alt="t('common.nav.backToHub')" class="w-5 h-5 shrink-0" />
<span class="text-sm font-medium">{{ t('common.nav.backToHub') }}</span>
</div>
</a>
-->
<PreferencesRow layout="list" />
</div>
<!-- Logged-out: prominent log-in CTA -->
<div v-if="!isAuthenticated" class="mt-6">
<Separator class="mb-4" />
<Button class="w-full" @click="goLogin">
<LogIn class="mr-2 h-4 w-4" />
{{ t('common.nav.login') }}
</Button>
</div>
<!-- Logged-in: log-out button stays visible without opening the edit popup. -->
<div v-else class="mt-6">
<Separator class="mb-4" />
<AlertDialog>
<AlertDialogTrigger as-child>
<Button variant="destructive" class="w-full">
<LogOut class="mr-2 h-4 w-4" />
{{ t('common.nav.logOut', 'Log out') }}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Log out of {{ user?.username || 'your account' }}?
</AlertDialogTitle>
<AlertDialogDescription>
{{ t('common.nav.logOutConfirmDescription', "You'll need to sign in again to access your wallet, post in the forum, place orders, or use any feature that needs your account.") }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('common.nav.cancel', 'Cancel') }}</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="onLogout"
> >
{{ t('common.nav.logOut', 'Log out') }} <Pencil class="h-4 w-4" />
</AlertDialogAction> </Button>
</AlertDialogFooter> </div>
</AlertDialogContent>
</AlertDialog> <!-- Identifier rows: full-width value with a corner-offset "legend"
badge straddling the top border (fieldset-legend pattern). The
value gets the entire row so long bech32 / username@domain
strings have room to render. -->
<div class="space-y-3 pt-1">
<!-- Lightning Address this is also the NIP-05 in this stack,
but the @username above already signals the NIP-05. -->
<button
v-if="lightningAddress"
type="button"
class="relative w-full rounded-md border bg-background/60 px-3 pt-3 pb-2 text-left hover:bg-background transition-colors min-w-0"
:aria-label="t('common.nav.copyLightning', 'Copy Lightning address')"
@click="copyToClipboard(lightningAddress, 'lightning')"
>
<span class="absolute -top-2 left-2 inline-flex items-center gap-1 rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground leading-none">
<Zap class="w-3 h-3 text-yellow-500 fill-yellow-500" />
{{ t('common.nav.lightning', 'Lightning') }}
</span>
<span class="block truncate pr-6 text-xs font-mono">{{ lightningAddress }}</span>
<component
:is="copiedField === 'lightning' ? Check : Copy"
class="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground"
/>
</button>
<!-- npub copy the full bech32 even though we display a preview. -->
<button
v-if="npub"
type="button"
class="relative w-full rounded-md border bg-background/60 px-3 pt-3 pb-2 text-left hover:bg-background transition-colors min-w-0"
:aria-label="t('common.nav.copyNpub', 'Copy npub')"
@click="copyToClipboard(npub, 'npub')"
>
<span class="absolute -top-2 left-2 inline-flex items-center rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground leading-none">
{{ t('common.nav.npub', 'npub') }}
</span>
<span class="block truncate pr-6 text-xs font-mono">{{ npubPreview }}</span>
<component
:is="copiedField === 'npub' ? Check : Copy"
class="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground"
/>
</button>
</div>
</div>
<!-- App-specific nav items (rendered by callers like StandaloneMenu) -->
<slot name="app-nav" />
<!-- Cross-app links + global preferences (always visible, auth or not) -->
<div class="mt-4">
<PreferencesRow layout="list" />
</div>
</div>
<!-- Pinned footer: stays stuck to the bottom of the sheet (shrink-0);
"Back to hub" sits directly above the log-in/out bar. -->
<div class="shrink-0 pt-1">
<!-- "Back to hub" shows the HUB's brand-kit logo (the brand's
primary/global logo via @brand-hub-logo) NOT the per-standalone
@brand-app-logo, which resolves to this standalone's own logo. -->
<a
:href="hubRootUrl"
class="flex items-center justify-center gap-3 px-3 py-1.5 hover:bg-accent rounded-md transition-colors"
:aria-label="t('common.nav.backToHub')"
>
<div class="flex items-center gap-3">
<img src="@brand-hub-logo" :alt="t('common.nav.backToHub')" class="w-8 h-8 shrink-0" />
<span class="text-sm font-medium">{{ t('common.nav.backToHub') }}</span>
</div>
</a>
<!-- Logged-out: prominent log-in CTA -->
<div v-if="!isAuthenticated">
<Separator class="mb-2" />
<Button class="w-full" @click="goLogin">
<LogIn class="mr-2 h-4 w-4" />
{{ t('common.nav.login') }}
</Button>
</div>
<!-- Logged-in: log-out button stays visible without opening the edit popup. -->
<div v-else>
<Separator class="mb-2" />
<AlertDialog>
<AlertDialogTrigger as-child>
<Button variant="destructive" class="w-full">
<LogOut class="mr-2 h-4 w-4" />
{{ t('common.nav.logOut', 'Log out') }}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Log out of {{ user?.username || 'your account' }}?
</AlertDialogTitle>
<AlertDialogDescription>
{{ t('common.nav.logOutConfirmDescription', "You'll need to sign in again to access your wallet, post in the forum, place orders, or use any feature that needs your account.") }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('common.nav.cancel', 'Cancel') }}</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="onLogout"
>
{{ t('common.nav.logOut', 'Log out') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</div> </div>
<!-- Edit-profile popup (pencil button in the identity card) the full <!-- Edit-profile popup (pencil button in the identity card) the full
form lives here so the sheet stays scannable. --> form lives here so the sheet stays scannable. Outside the flex root:
its content portals to <body>, so it's not part of the sheet flow. -->
<Dialog v-model:open="editProfileOpen"> <Dialog v-model:open="editProfileOpen">
<DialogContent class="max-w-md max-h-[90vh] overflow-y-auto overflow-x-hidden"> <DialogContent class="max-w-md max-h-[90vh] overflow-y-auto overflow-x-hidden">
<DialogHeader> <DialogHeader>

View file

@ -115,6 +115,22 @@ export function brandAppLogoAliasEntry(app?: string) {
} as const } as const
} }
/**
* Hub-logo alias entry. Resolves `@brand-hub-logo` to the brand's
* primary/global logo (the hub's logo), independent of which standalone
* is building. Unlike {@link brandAppLogoAliasEntry}, this never takes an
* `app` argument the "Back to hub" link in every standalone must point
* at the HUB's logo, not the current standalone's own logo. Wire it into
* every vite.<app>.config.ts that builds ProfileSheetContent.vue.
*/
export function brandHubLogoAliasEntry() {
const resolved = resolveAppLogo()
return {
find: /^@brand-hub-logo(\?.*)?$/,
replacement: `${resolved}$1`,
} as const
}
/** /**
* Optional brand banner a wide lockup (logo + wordmark in one image) * Optional brand banner a wide lockup (logo + wordmark in one image)
* that replaces the logo + app-name pair in a standalone's header. * that replaces the logo + app-name pair in a standalone's header.

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
function chatHtmlPlugin(): Plugin { function chatHtmlPlugin(): Plugin {
return { return {
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('chat'), brandAppLogoAliasEntry('chat'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)) },

View file

@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
// https://vite.dev/config/ // https://vite.dev/config/
// //
@ -53,6 +53,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare alias. // with optional `?url` query) doesn't get shadowed by the bare alias.
alias: [ alias: [
brandAppLogoAliasEntry(), brandAppLogoAliasEntry(),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
] ]

View file

@ -11,6 +11,7 @@ import {
brandAppBannerAliasEntry, brandAppBannerAliasEntry,
brandAppLogoAliasEntry, brandAppLogoAliasEntry,
brandAssetsPlugin, brandAssetsPlugin,
brandHubLogoAliasEntry,
brandManifestName, brandManifestName,
resolveAppBanner, resolveAppBanner,
} from './vite-branding' } from './vite-branding'
@ -137,6 +138,7 @@ export default defineConfig(({ mode }) => ({
alias: [ alias: [
brandAppLogoAliasEntry('events'), brandAppLogoAliasEntry('events'),
brandAppBannerAliasEntry('events'), brandAppBannerAliasEntry('events'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
], ],

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
function forumHtmlPlugin(): Plugin { function forumHtmlPlugin(): Plugin {
return { return {
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('forum'), brandAppLogoAliasEntry('forum'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)) },

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to libra.html * Plugin to rewrite dev server requests to libra.html
@ -114,6 +114,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('libra'), brandAppLogoAliasEntry('libra'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @rollup/plugin-alias is first-match-wins. // ORDER MATTERS — @rollup/plugin-alias is first-match-wins.
// The more specific @/app.config remap must precede the @ prefix // The more specific @/app.config remap must precede the @ prefix

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
function marketHtmlPlugin(): Plugin { function marketHtmlPlugin(): Plugin {
return { return {
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('market'), brandAppLogoAliasEntry('market'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)) },

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
function restaurantHtmlPlugin(): Plugin { function restaurantHtmlPlugin(): Plugin {
return { return {
@ -114,6 +114,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('restaurant'), brandAppLogoAliasEntry('restaurant'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/restaurant-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/restaurant-app/app.config.ts', import.meta.url)) },

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
function tasksHtmlPlugin(): Plugin { function tasksHtmlPlugin(): Plugin {
return { return {
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('tasks'), brandAppLogoAliasEntry('tasks'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)) },

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin } from './vite-branding' import { brand, brandAlias, brandAppLogoAliasEntry, brandAssetsPlugin, brandHubLogoAliasEntry } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to wallet.html * Plugin to rewrite dev server requests to wallet.html
@ -113,6 +113,7 @@ export default defineConfig(({ mode }) => ({
// with optional `?url` query) doesn't get shadowed by the bare aliases. // with optional `?url` query) doesn't get shadowed by the bare aliases.
alias: [ alias: [
brandAppLogoAliasEntry('wallet'), brandAppLogoAliasEntry('wallet'),
brandHubLogoAliasEntry(),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
{ find: '@/app.config', replacement: fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)) }, { find: '@/app.config', replacement: fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)) },