mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
feat(auth): footer parity with marketing site on auth-page family (TASK-903) (#314)
* feat(auth): footer parity with marketing site on auth-page family (TASK-903)
New <AuthFooter cloudMode={...} /> replaces the prior LegalFooter +
SupportFooter pair. Single component matches the brand spec
(docs/brand.md §7) which describes ONE footer pattern, not two
separate strips.
Cloud mode (cloudMode=true) carries the full getpad.dev marketing
footer: copyright line ("© <year> Pad · Perpetual Software") + the
nine-link list in canonical order — GitHub, Docs, Changelog,
Contribute, FAQ, Security, Privacy, Terms, Sub-processors. Visual
contract anchored on pad-web/src/routes/+layout.svelte (border-top,
max-w-6xl, flex-wrap, sm: breakpoint at 640px).
Self-hosted (cloudMode=false) renders the legal-essentials only —
Terms / Privacy / Sub-processors — preserving the visual treatment of
the prior LegalFooter exactly so existing self-hosted deployments see
no change after this PR. The Status / Support / GitHub / Changelog /
Contribute / FAQ / Security links were Cloud-only in the prior shape
too; that stays the case.
Wired the new AuthFooter into all five auth-family pages:
- /login (replaces LegalFooter + SupportFooter)
- /register (replaces LegalFooter + SupportFooter)
- /forgot-password (replaces LegalFooter + SupportFooter)
- /reset-password/[token] (NEW — was footer-less)
- /join/[code] (NEW — was footer-less)
LegalFooter.svelte and SupportFooter.svelte are deleted; they were
internal to the auth-pages feature and never used elsewhere
(grep-verified). AuthHeader's comment that referenced them is
updated to point at AuthFooter instead.
Year is computed once per page render via new Date().getFullYear()
— no auto-refresh needed since auth pages don't sit open across a
year boundary in any realistic flow.
Parent: PLAN-900.
Test plan:
- web/npm run check — 0 errors (692 files now, was 693; net -1 reflects
2 deletions + 1 addition)
- web/npm run build — clean
- go build ./... && go vet ./... && go test ./... — all pass
- Svelte autofixer — clean
* fix(auth): self-hosted AuthFooter renders nothing per Codex review (round 2)
Codex P1: the prior LegalFooter + SupportFooter both gated their entire
body on `{#if cloudMode}` — i.e. self-hosted rendered nothing at all.
The first draft of AuthFooter incorrectly assumed self-hosted should
get the legal-essentials subset (Terms / Privacy / Sub-processors
links), which would have rendered getpad.dev's hosted-service legal
links on someone else's deployment, misrepresenting the operator's
own legal terms.
Revert the self-hosted branch to render nothing. The brand spec
(docs/brand.md §7) already flags an operator-owned legal/footer
mechanism as deferred to the operator-branding follow-up plan, so
this restores the prior behavior exactly.
Removed the now-dead self-hosted link list, the .auth-footer-legal
CSS rules, and the $derived links computation.
* fix(auth): flex-direction on reset-password + join wrappers per Codex (round 3)
Codex P2: AuthFooter on /reset-password/[token] and /join/[code] sat
horizontally next to the auth card instead of below it because those
two page wrappers were `display: flex` without `flex-direction: column`.
The other three auth pages (login, register, forgot-password) already
had column layout so they were unaffected — only the two pages that
gained a footer in this PR were broken.
Add `flex-direction: column` to .page (reset-password) and .join-page
so the footer renders below the card on those routes too.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
// Footer rendered below the auth-page family.
|
||||
//
|
||||
// On Pad Cloud (cloudMode=true) this carries the full getpad.dev marketing
|
||||
// footer link list — same canonical order as pad-web/src/routes/+layout.svelte
|
||||
// — so the auth pages feel like one continuous property with the marketing
|
||||
// site rather than a stripped-down stub.
|
||||
//
|
||||
// On self-hosted (cloudMode=false) this renders NOTHING. Operators ship
|
||||
// Pad under their own brand and have their own legal docs; rendering
|
||||
// getpad.dev Terms/Privacy/Sub-processors links would misrepresent the
|
||||
// operator's legal terms as if they were the deployment's own. This
|
||||
// matches the prior LegalFooter + SupportFooter behavior (both gated
|
||||
// their entire body on `{#if cloudMode}`) and the brand spec
|
||||
// (docs/brand.md §7) note that an operator-owned legal/footer
|
||||
// mechanism is deferred to the operator-branding follow-up plan.
|
||||
//
|
||||
// Visual contract: docs/brand.md, section 7. Source of truth for tokens,
|
||||
// link order, and copyright format is pad-web/src/routes/+layout.svelte.
|
||||
// When changing those, update the brand doc and this file together.
|
||||
//
|
||||
// Replaces the prior LegalFooter + SupportFooter pair — the brand spec
|
||||
// describes a single footer pattern, not two separate strips.
|
||||
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
|
||||
// Canonical Cloud-mode link order from docs/brand.md §7. Do not reorder.
|
||||
// All Cloud links are off-property (github.com or getpad.dev) so every
|
||||
// anchor opens in a new tab — the user is mid-auth-flow and we don't want
|
||||
// to lose their form state by navigating away.
|
||||
const cloudLinks: Array<{ label: string; href: string }> = [
|
||||
{ label: 'GitHub', href: 'https://github.com/PerpetualSoftware/pad' },
|
||||
{ label: 'Docs', href: 'https://getpad.dev/docs' },
|
||||
{ label: 'Changelog', href: 'https://getpad.dev/changelog' },
|
||||
{ label: 'Contribute', href: 'https://getpad.dev/contribute' },
|
||||
{ label: 'FAQ', href: 'https://getpad.dev/faq' },
|
||||
{ label: 'Security', href: 'https://getpad.dev/security' },
|
||||
{ label: 'Privacy', href: 'https://getpad.dev/privacy' },
|
||||
{ label: 'Terms', href: 'https://getpad.dev/terms' },
|
||||
{ label: 'Sub-processors', href: 'https://getpad.dev/subprocessors' }
|
||||
];
|
||||
|
||||
// Year is computed once per page render — no auto-refresh, but auth pages
|
||||
// don't sit open across a year boundary in any realistic flow.
|
||||
const year = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
{#if cloudMode}
|
||||
<footer class="auth-footer cloud">
|
||||
<div class="auth-footer-row">
|
||||
<p class="auth-footer-copyright">
|
||||
© {year} Pad
|
||||
<span class="auth-footer-sep" aria-hidden="true">·</span>
|
||||
<a
|
||||
href="https://perpetualsoftware.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="auth-footer-perpetual"
|
||||
>
|
||||
Perpetual Software
|
||||
</a>
|
||||
</p>
|
||||
<nav class="auth-footer-links" aria-label="Site navigation">
|
||||
{#each cloudLinks as link (link.label)}
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="auth-footer-link"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
<!-- Self-hosted (cloudMode === false) intentionally renders nothing — see
|
||||
the doc-comment in the script block. The operator-customizable footer
|
||||
is a separate, larger plan that will land after PLAN-900 ships. -->
|
||||
|
||||
<style>
|
||||
/* Cloud footer — full marketing-style strip. Matches pad-web's footer
|
||||
structure: a top border, max-w-6xl container, copyright line on the
|
||||
left and link row on the right. Stacks vertically on small screens. */
|
||||
.auth-footer.cloud {
|
||||
margin-top: var(--space-10);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding-top: var(--space-6);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-footer-row {
|
||||
max-width: 72rem; /* matches max-w-6xl */
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-footer-copyright {
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-footer-sep {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.auth-footer-perpetual {
|
||||
color: color-mix(in srgb, var(--text-muted) 60%, transparent);
|
||||
text-decoration: none;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.auth-footer-perpetual:hover,
|
||||
.auth-footer-perpetual:focus-visible {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: var(--space-6);
|
||||
row-gap: var(--space-3);
|
||||
}
|
||||
|
||||
.auth-footer-link {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.auth-footer-link:hover,
|
||||
.auth-footer-link:focus-visible {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* sm breakpoint = 640px (Tailwind default; matches pad-web's sm:flex-row). */
|
||||
@media (min-width: 640px) {
|
||||
.auth-footer-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,7 @@
|
||||
//
|
||||
// Self-hosted (cloudMode === false) renders nothing — operators ship Pad
|
||||
// under their own brand and must not get getpad.dev chrome imposed on them.
|
||||
// This matches the existing pattern in LegalFooter.svelte / SupportFooter.svelte.
|
||||
// Same pattern as the companion AuthFooter.svelte in this directory.
|
||||
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts">
|
||||
// Legal links shown below the auth card on Pad Cloud (cloudMode=true).
|
||||
// Self-hosted installs do not need these links — the Terms/Privacy that
|
||||
// live at getpad.dev are Perpetual Software LLC's for the hosted service,
|
||||
// not for the user's own instance.
|
||||
//
|
||||
// Caller typically reads cloudMode from authStore (which the root layout
|
||||
// already populates via authStore.load()), so no additional session fetch
|
||||
// is needed. Links open in a new tab so the auth flow is not interrupted.
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
</script>
|
||||
|
||||
{#if cloudMode}
|
||||
<nav class="legal-footer" aria-label="Legal">
|
||||
<a href="https://getpad.dev/terms" target="_blank" rel="noopener noreferrer">Terms</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href="https://getpad.dev/privacy" target="_blank" rel="noopener noreferrer">Privacy</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href="https://getpad.dev/subprocessors" target="_blank" rel="noopener noreferrer">Sub-processors</a>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.legal-footer {
|
||||
margin-top: var(--space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.legal-footer a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legal-footer a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.legal-footer a:focus-visible {
|
||||
color: var(--text-primary);
|
||||
outline: 2px solid var(--accent-blue);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
// Support/help links shown below the auth card on Pad Cloud.
|
||||
//
|
||||
// Gated on cloudMode — the support@getpad.dev mailbox and status.getpad.dev
|
||||
// page are for the hosted service, not for self-hosted deployments. Caller
|
||||
// typically reads cloudMode from authStore. Links open in a new tab so
|
||||
// clicking one mid-login does not lose the form state.
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
</script>
|
||||
|
||||
{#if cloudMode}
|
||||
<nav class="support-footer" aria-label="Support">
|
||||
<a href="mailto:support@getpad.dev">Support</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href="https://status.getpad.dev" target="_blank" rel="noopener noreferrer">Status</a>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.support-footer {
|
||||
margin-top: var(--space-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.support-footer a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.support-footer a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.support-footer a:focus-visible {
|
||||
color: var(--text-primary);
|
||||
outline: 2px solid var(--accent-blue);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,7 @@
|
||||
import { api } from '$lib/api/client';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AuthHeader from '$lib/components/auth/AuthHeader.svelte';
|
||||
import LegalFooter from '$lib/components/auth/LegalFooter.svelte';
|
||||
import SupportFooter from '$lib/components/auth/SupportFooter.svelte';
|
||||
import AuthFooter from '$lib/components/auth/AuthFooter.svelte';
|
||||
|
||||
let email = $state('');
|
||||
let error = $state('');
|
||||
@@ -100,8 +99,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<LegalFooter cloudMode={authStore.cloudMode} />
|
||||
<SupportFooter cloudMode={authStore.cloudMode} />
|
||||
<AuthFooter cloudMode={authStore.cloudMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import SetupRequiredNotice from '$lib/components/auth/SetupRequiredNotice.svelte';
|
||||
import AuthHeader from '$lib/components/auth/AuthHeader.svelte';
|
||||
import AuthFooter from '$lib/components/auth/AuthFooter.svelte';
|
||||
|
||||
let code = $derived(page.params.code ?? '');
|
||||
let status = $state<'loading' | 'login' | 'register' | 'accepting' | 'error' | 'setup' | '2fa'>('loading');
|
||||
@@ -339,11 +340,14 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<AuthFooter cloudMode={authStore.cloudMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.join-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import SetupRequiredNotice from '$lib/components/auth/SetupRequiredNotice.svelte';
|
||||
import AuthHeader from '$lib/components/auth/AuthHeader.svelte';
|
||||
import LegalFooter from '$lib/components/auth/LegalFooter.svelte';
|
||||
import SupportFooter from '$lib/components/auth/SupportFooter.svelte';
|
||||
import AuthFooter from '$lib/components/auth/AuthFooter.svelte';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
@@ -391,8 +390,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<LegalFooter {cloudMode} />
|
||||
<SupportFooter {cloudMode} />
|
||||
<AuthFooter {cloudMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import SetupRequiredNotice from '$lib/components/auth/SetupRequiredNotice.svelte';
|
||||
import AuthHeader from '$lib/components/auth/AuthHeader.svelte';
|
||||
import LegalFooter from '$lib/components/auth/LegalFooter.svelte';
|
||||
import SupportFooter from '$lib/components/auth/SupportFooter.svelte';
|
||||
import AuthFooter from '$lib/components/auth/AuthFooter.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let name = $state('');
|
||||
@@ -234,8 +233,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<LegalFooter cloudMode={authStore.cloudMode} />
|
||||
<SupportFooter cloudMode={authStore.cloudMode} />
|
||||
<AuthFooter cloudMode={authStore.cloudMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { api } from '$lib/api/client';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AuthHeader from '$lib/components/auth/AuthHeader.svelte';
|
||||
import AuthFooter from '$lib/components/auth/AuthFooter.svelte';
|
||||
|
||||
let token = $derived(page.params.token ?? '');
|
||||
|
||||
@@ -101,11 +102,14 @@
|
||||
<a href="/login">Back to sign in</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AuthFooter cloudMode={authStore.cloudMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
|
||||
Reference in New Issue
Block a user