feat(layout): re-enable "Back to hub" with a sticky sheet footer #128
11 changed files with 183 additions and 149 deletions
|
|
@ -66,9 +66,7 @@ const npubPreview = computed(() => {
|
|||
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value
|
||||
})
|
||||
|
||||
// "Back to hub" is hidden for the events-only launch (see template).
|
||||
// Re-enable this alongside the commented-out link below when the hub ships.
|
||||
// const hubRootUrl = computed(() => import.meta.env.VITE_HUB_ROOT_URL || '/')
|
||||
const hubRootUrl = computed(() => import.meta.env.VITE_HUB_ROOT_URL || '/')
|
||||
|
||||
const copiedField = ref<string | null>(null)
|
||||
async function copyToClipboard(text: string, field: string) {
|
||||
|
|
@ -105,6 +103,14 @@ async function onLogout() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Fill the sheet exactly (h-full) so the footer stays stuck to the
|
||||
bottom while only the region above it scrolls. The sheet host already
|
||||
has overflow-y-auto, but with an exact-fit child it never triggers —
|
||||
the inner flex-1 region owns the scroll instead. -->
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Scrollable region: everything above the pinned footer. min-h-0 lets
|
||||
this flex child shrink below its content height so it can scroll. -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{{ t('common.nav.profile') }}</SheetTitle>
|
||||
<SheetDescription v-if="isAuthenticated">
|
||||
|
|
@ -190,31 +196,30 @@ async function onLogout() {
|
|||
|
||||
<!-- 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.
|
||||
<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-between gap-3 px-3 py-3 hover:bg-accent rounded-md transition-colors"
|
||||
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-5 h-5 shrink-0" />
|
||||
<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>
|
||||
-->
|
||||
|
||||
<PreferencesRow layout="list" />
|
||||
</div>
|
||||
|
||||
<!-- Logged-out: prominent log-in CTA -->
|
||||
<div v-if="!isAuthenticated" class="mt-6">
|
||||
<Separator class="mb-4" />
|
||||
<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') }}
|
||||
|
|
@ -222,8 +227,8 @@ async function onLogout() {
|
|||
</div>
|
||||
|
||||
<!-- Logged-in: log-out button stays visible without opening the edit popup. -->
|
||||
<div v-else class="mt-6">
|
||||
<Separator class="mb-4" />
|
||||
<div v-else>
|
||||
<Separator class="mb-2" />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button variant="destructive" class="w-full">
|
||||
|
|
@ -252,9 +257,12 @@ async function onLogout() {
|
|||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<DialogContent class="max-w-md max-h-[90vh] overflow-y-auto overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,22 @@ export function brandAppLogoAliasEntry(app?: string) {
|
|||
} 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)
|
||||
* that replaces the logo + app-name pair in a standalone's header.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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 {
|
||||
return {
|
||||
|
|
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('chat'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
|
|||
import Inspect from 'vite-plugin-inspect'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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/
|
||||
//
|
||||
|
|
@ -53,6 +53,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare alias.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry(),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
brandAppBannerAliasEntry,
|
||||
brandAppLogoAliasEntry,
|
||||
brandAssetsPlugin,
|
||||
brandHubLogoAliasEntry,
|
||||
brandManifestName,
|
||||
resolveAppBanner,
|
||||
} from './vite-branding'
|
||||
|
|
@ -137,6 +138,7 @@ export default defineConfig(({ mode }) => ({
|
|||
alias: [
|
||||
brandAppLogoAliasEntry('events'),
|
||||
brandAppBannerAliasEntry('events'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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 {
|
||||
return {
|
||||
|
|
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('forum'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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
|
||||
|
|
@ -114,6 +114,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('libra'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// ORDER MATTERS — @rollup/plugin-alias is first-match-wins.
|
||||
// The more specific @/app.config remap must precede the @ prefix
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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 {
|
||||
return {
|
||||
|
|
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('market'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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 {
|
||||
return {
|
||||
|
|
@ -114,6 +114,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('restaurant'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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 {
|
||||
return {
|
||||
|
|
@ -107,6 +107,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('tasks'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
|
|||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
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
|
||||
|
|
@ -113,6 +113,7 @@ export default defineConfig(({ mode }) => ({
|
|||
// with optional `?url` query) doesn't get shadowed by the bare aliases.
|
||||
alias: [
|
||||
brandAppLogoAliasEntry('wallet'),
|
||||
brandHubLogoAliasEntry(),
|
||||
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
|
||||
// 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)) },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue