From eaacb3b9855e7f423ce3cca0d6ea9d2ef1a3164b Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 7 May 2026 12:05:56 +0200 Subject: [PATCH] feat(layout): unified app-shell primitives (Phase A, no consumer changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build the shared building blocks for the unified bottom-nav UX across the hub + 7 standalones. Phase A is groundwork only — no App.vue or Hub.vue consumer is wired up yet, so this commit is purely additive. New components in src/components/layout/: - PreferencesRow.vue theme/language/currency triad (row + list layouts) - ProfileSheetContent.vue identity card + back-to-hub + prefs + ProfileSettings - ProfileSheetTrigger.vue bottom-row Profile button → opens sheet - HubPill.vue fixed top-right back-to-hub link - BottomNav.vue consumer tabs + appended Profile slot - AppShell.vue outer wrapper composing the above New composable: useCurrentUserAvatar — picture/displayName/fallbackInitial from the auth user object. i18n: new common.nav.* namespace in en/es/fr (typed via LocaleMessages). Env: VITE_HUB_ROOT_URL added to .env.example with path/subdomain/local guidance — consumed by HubPill and the back-to-hub sheet item. Phase B (consumer refactor: chat/wallet/tasks first, then forum/libra/ market/activities, then hub) lands separately. --- .env.example | 19 +++ src/components/layout/AppShell.vue | 55 +++++++ src/components/layout/BottomNav.vue | 65 ++++++++ src/components/layout/HubPill.vue | 24 +++ src/components/layout/PreferencesRow.vue | 152 ++++++++++++++++++ src/components/layout/ProfileSheetContent.vue | 86 ++++++++++ src/components/layout/ProfileSheetTrigger.vue | 68 ++++++++ src/composables/useCurrentUserAvatar.ts | 42 +++++ src/i18n/locales/en.ts | 19 ++- src/i18n/locales/es.ts | 19 ++- src/i18n/locales/fr.ts | 19 ++- src/i18n/types.ts | 17 ++ 12 files changed, 582 insertions(+), 3 deletions(-) create mode 100644 src/components/layout/AppShell.vue create mode 100644 src/components/layout/BottomNav.vue create mode 100644 src/components/layout/HubPill.vue create mode 100644 src/components/layout/PreferencesRow.vue create mode 100644 src/components/layout/ProfileSheetContent.vue create mode 100644 src/components/layout/ProfileSheetTrigger.vue create mode 100644 src/composables/useCurrentUserAvatar.ts diff --git a/.env.example b/.env.example index 9e80e61..63b30aa 100644 --- a/.env.example +++ b/.env.example @@ -87,6 +87,25 @@ VITE_HUB_FORUM_URL= VITE_HUB_MARKET_URL= VITE_HUB_TASKS_URL= +# ─────────────────────────────────────────────────────────────────────── +# VITE_HUB_ROOT_URL — standalone → hub (the inverse of the URLs above) +# +# Read by the standalone shell's (top-right "back to hub" link) +# and the "Back to Hub" item inside the profile sheet. Each standalone's +# bundle gets this value baked in at build time. +# +# In PATH-MODE deployment the standalone and hub share an origin, so the +# default ('/' if unset) is correct — the link is same-origin. +# +# In SUBDOMAIN-MODE production the standalone is on a different origin +# than the hub; set this to the full hub URL: +# VITE_HUB_ROOT_URL=https://app.example.com/ +# +# In LOCAL DEV with `npm run dev:all`, the hub is on :5173: +# VITE_HUB_ROOT_URL=http://localhost:5173/ +# ─────────────────────────────────────────────────────────────────────── +VITE_HUB_ROOT_URL= + # ─────────────────────────────────────────────────────────────────────── # VITE_BASE_PATH — build-time only, NOT per .env # diff --git a/src/components/layout/AppShell.vue b/src/components/layout/AppShell.vue new file mode 100644 index 0000000..9bfacbd --- /dev/null +++ b/src/components/layout/AppShell.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/layout/BottomNav.vue b/src/components/layout/BottomNav.vue new file mode 100644 index 0000000..e74b67d --- /dev/null +++ b/src/components/layout/BottomNav.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/layout/HubPill.vue b/src/components/layout/HubPill.vue new file mode 100644 index 0000000..dbe4f5a --- /dev/null +++ b/src/components/layout/HubPill.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/layout/PreferencesRow.vue b/src/components/layout/PreferencesRow.vue new file mode 100644 index 0000000..717339b --- /dev/null +++ b/src/components/layout/PreferencesRow.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/components/layout/ProfileSheetContent.vue b/src/components/layout/ProfileSheetContent.vue new file mode 100644 index 0000000..d08735a --- /dev/null +++ b/src/components/layout/ProfileSheetContent.vue @@ -0,0 +1,86 @@ + + + diff --git a/src/components/layout/ProfileSheetTrigger.vue b/src/components/layout/ProfileSheetTrigger.vue new file mode 100644 index 0000000..9e1b61b --- /dev/null +++ b/src/components/layout/ProfileSheetTrigger.vue @@ -0,0 +1,68 @@ + + + diff --git a/src/composables/useCurrentUserAvatar.ts b/src/composables/useCurrentUserAvatar.ts new file mode 100644 index 0000000..b69c27c --- /dev/null +++ b/src/composables/useCurrentUserAvatar.ts @@ -0,0 +1,42 @@ +import { computed } from 'vue' +import { useAuth } from '@/composables/useAuthService' + +/** + * Surface the current user's display identity in the form needed by avatar + * components: a `picture` URL (Nostr kind-0 metadata, mirrored into LNbits + * `extra.picture` by ProfileSettings on save), a friendly display name, and + * a single-character fallback for `` when no picture loads. + * + * Returns null-ish values when unauthenticated so consumers can render a + * generic icon (LogIn, UserIcon) without optional-chain noise. + */ +export function useCurrentUserAvatar() { + const { user, isAuthenticated } = useAuth() + + const pictureUrl = computed(() => { + if (!isAuthenticated.value) return null + return user.value?.extra?.picture || null + }) + + const displayName = computed(() => { + if (!isAuthenticated.value) return null + return user.value?.extra?.display_name || user.value?.username || null + }) + + /** First non-whitespace character of display name, uppercased. Empty when + * unauthenticated or no name available — consumer should render a fallback + * icon in that case. */ + const fallbackInitial = computed(() => { + const name = displayName.value + if (!name) return '' + const trimmed = name.trim() + return trimmed.length > 0 ? trimmed.charAt(0).toUpperCase() : '' + }) + + return { + isAuthenticated, + pictureUrl, + displayName, + fallbackInitial, + } +} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 09d0dd2..daeb845 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -16,7 +16,24 @@ const messages: LocaleMessages = { common: { loading: 'Loading...', error: 'An error occurred', - success: 'Operation successful' + success: 'Operation successful', + nav: { + profile: 'Profile', + preferences: 'Preferences', + profileDescription: 'Your Nostr identity and display name.', + profileLoggedOutDescription: 'Sign in or change your preferences.', + login: 'Log in', + backToHub: 'Back to hub', + hub: 'Hub', + theme: 'Theme', + themeLight: 'Light', + themeDark: 'Dark', + themeSystem: 'System', + language: 'Language', + currency: 'Currency', + currencyComingSoon: 'Currency picker — coming soon', + currencyComingSoonDescription: 'A preferred-currency setting (sats/USD/EUR) is on the roadmap.' + } }, errors: { notFound: 'Page not found', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index f4952c1..76a5f56 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -16,7 +16,24 @@ const messages: LocaleMessages = { common: { loading: 'Cargando...', error: 'Ha ocurrido un error', - success: 'Operación exitosa' + success: 'Operación exitosa', + nav: { + profile: 'Perfil', + preferences: 'Preferencias', + profileDescription: 'Tu identidad Nostr y nombre de visualización.', + profileLoggedOutDescription: 'Inicia sesión o cambia tus preferencias.', + login: 'Iniciar sesión', + backToHub: 'Volver al hub', + hub: 'Hub', + theme: 'Tema', + themeLight: 'Claro', + themeDark: 'Oscuro', + themeSystem: 'Sistema', + language: 'Idioma', + currency: 'Moneda', + currencyComingSoon: 'Selector de moneda — próximamente', + currencyComingSoonDescription: 'Un ajuste de moneda preferida (sats/USD/EUR) está en la hoja de ruta.' + } }, errors: { notFound: 'Página no encontrada', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 514f978..19b8ece 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -16,7 +16,24 @@ const messages: LocaleMessages = { common: { loading: 'Chargement...', error: 'Une erreur est survenue', - success: 'Opération réussie' + success: 'Opération réussie', + nav: { + profile: 'Profil', + preferences: 'Préférences', + profileDescription: 'Votre identité Nostr et votre nom d\u2019affichage.', + profileLoggedOutDescription: 'Connectez-vous ou modifiez vos préférences.', + login: 'Se connecter', + backToHub: 'Retour au hub', + hub: 'Hub', + theme: 'Thème', + themeLight: 'Clair', + themeDark: 'Sombre', + themeSystem: 'Système', + language: 'Langue', + currency: 'Devise', + currencyComingSoon: 'Sélecteur de devise — bientôt disponible', + currencyComingSoonDescription: 'Un réglage de devise préférée (sats/USD/EUR) est prévu.' + } }, errors: { notFound: 'Page non trouvée', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 23dd1f9..0ca44dd 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -16,6 +16,23 @@ export interface LocaleMessages { loading: string error: string success: string + nav: { + profile: string + preferences: string + profileDescription: string + profileLoggedOutDescription: string + login: string + backToHub: string + hub: string + theme: string + themeLight: string + themeDark: string + themeSystem: string + language: string + currency: string + currencyComingSoon: string + currencyComingSoonDescription: string + } } errors: { notFound: string