@djangocfg/layouts 2.1.585 → 2.1.590
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/package.json +12 -12
- package/src/layouts/AuthLayout/AuthLayout.tsx +13 -2
- package/src/layouts/AuthLayout/README.md +32 -0
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +0 -22
- package/src/layouts/AuthLayout/components/shared/AuthTopBar.tsx +65 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +2 -0
- package/src/layouts/AuthLayout/shells/AuthShell.tsx +2 -0
- package/src/layouts/AuthLayout/shells/CenteredShell.tsx +3 -0
- package/src/layouts/AuthLayout/shells/FullSplitShell.tsx +2 -0
- package/src/layouts/AuthLayout/shells/SplitShell.tsx +2 -0
- package/src/layouts/AuthLayout/shells/types.ts +8 -0
- package/src/layouts/AuthLayout/styles/auth.css +55 -12
- package/src/layouts/AuthLayout/styles/centered-shell.css +11 -2
- package/src/layouts/AuthLayout/styles/fullsplit-shell.css +12 -4
- package/src/layouts/AuthLayout/styles/split-shell.css +14 -6
- package/src/testing/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.590",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"check": "tsc --noEmit"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@djangocfg/analytics": "^2.1.
|
|
94
|
-
"@djangocfg/api": "^2.1.
|
|
95
|
-
"@djangocfg/devtools": "^2.1.
|
|
96
|
-
"@djangocfg/i18n": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
+
"@djangocfg/analytics": "^2.1.590",
|
|
94
|
+
"@djangocfg/api": "^2.1.590",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.590",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.590",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.590",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -124,12 +124,12 @@
|
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/analytics": "^2.1.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/devtools": "^2.1.
|
|
130
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
132
|
-
"@djangocfg/ui-core": "^2.1.
|
|
127
|
+
"@djangocfg/analytics": "^2.1.590",
|
|
128
|
+
"@djangocfg/api": "^2.1.590",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.590",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.590",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.590",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.590",
|
|
133
133
|
"@storybook/react-vite": "^10.5.0",
|
|
134
134
|
"@types/node": "^25.9.5",
|
|
135
135
|
"@types/react": "19.2.15",
|
|
@@ -21,7 +21,7 @@ import { useAppT } from '@djangocfg/i18n';
|
|
|
21
21
|
|
|
22
22
|
import { Suspense } from '../../components';
|
|
23
23
|
import { TwoFactorChallengeDialog } from '../../snippets/TwoFactor';
|
|
24
|
-
import { AuthBrandPanel } from './components/shared';
|
|
24
|
+
import { AuthBrandPanel, AuthTopBar } from './components/shared';
|
|
25
25
|
import { OAuthCallback } from './components/oauth';
|
|
26
26
|
import { IdentifierStep, OTPStep } from './components/steps';
|
|
27
27
|
import { AUTH } from './constants';
|
|
@@ -113,6 +113,10 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
113
113
|
[hideHeader, hasBrandPanel],
|
|
114
114
|
);
|
|
115
115
|
|
|
116
|
+
// The way out and the locale, pinned to the top of the form column. Owned by
|
|
117
|
+
// the layout so it does not depend on `logo` or `sidebar`, which are optional.
|
|
118
|
+
const topBar = useMemo(() => <AuthTopBar brandPanel={hasBrandPanel} />, [hasBrandPanel]);
|
|
119
|
+
|
|
116
120
|
const oauthCallback = useMemo(() => {
|
|
117
121
|
if (!enableGithubAuth) return null;
|
|
118
122
|
return (
|
|
@@ -135,7 +139,14 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
135
139
|
<AuthFunnelTracker />
|
|
136
140
|
<AuthTwoFactorChallenge onVerified={onOTPSuccess} />
|
|
137
141
|
|
|
138
|
-
<AuthShell
|
|
142
|
+
<AuthShell
|
|
143
|
+
variant={variant}
|
|
144
|
+
mediaSide={mediaSide}
|
|
145
|
+
background={background}
|
|
146
|
+
sidebar={resolvedSidebar}
|
|
147
|
+
topBar={topBar}
|
|
148
|
+
className={className}
|
|
149
|
+
>
|
|
139
150
|
{/* Handle OAuth callback when GitHub auth is enabled */}
|
|
140
151
|
{oauthCallback}
|
|
141
152
|
|
|
@@ -51,6 +51,38 @@ The link carries `aria-label` from `layouts.navigation.home` rather than
|
|
|
51
51
|
relying on its contents: `logo` is an opaque node, and a brand that is a bare
|
|
52
52
|
SVG would otherwise announce as an unnamed link.
|
|
53
53
|
|
|
54
|
+
## The top bar: the way out and the language
|
|
55
|
+
|
|
56
|
+
Every variant pins one bar to the top of the form column: **`‹ Home`** on the
|
|
57
|
+
left, the **locale picker** on the right. The layout owns it (`AuthTopBar`,
|
|
58
|
+
passed to the shell as `topBar`), so it does not depend on `logo` or `sidebar`.
|
|
59
|
+
|
|
60
|
+
> It exists because the brand link above was the only exit, and the brand is
|
|
61
|
+
> optional. An app that passed its own `sidebar` and no `logo` (the Kino sign-in)
|
|
62
|
+
> left a phone visitor with no route off the screen: the media half is hidden
|
|
63
|
+
> below 1024px and the form header had nothing to render. The locale, meanwhile,
|
|
64
|
+
> floated above the title inside the centered stack.
|
|
65
|
+
|
|
66
|
+
- The link is a real `<a href="/">` (`AUTH.HOME_URL`) with a visible label, the
|
|
67
|
+
way an iOS back button names its destination. A bare arrow reads as browser
|
|
68
|
+
Back, and Back has nowhere to go after a direct link. The label is
|
|
69
|
+
`layouts.navigation.home`, translated in all 17 locales.
|
|
70
|
+
- Both controls are at least 44px tall. The bar clears the notch with
|
|
71
|
+
`env(safe-area-inset-top)`.
|
|
72
|
+
- When the layout's own `AuthBrandPanel` is on the photo half (`fullsplit`, `logo`
|
|
73
|
+
and no `sidebar`) the home link stands down from 1024px, since the panel links
|
|
74
|
+
home already. With a custom `sidebar` the bar's link stays: the layout cannot
|
|
75
|
+
know the sidebar links anywhere.
|
|
76
|
+
- The locale picker mounts whenever `LayoutI18nProvider` does (`BaseApp` does it
|
|
77
|
+
when the host passes `i18n`). It moved out of `AuthContainer`.
|
|
78
|
+
|
|
79
|
+
**Height.** Every shell is `min-height: 100dvh` with a `100vh` fallback, so the
|
|
80
|
+
mobile address bar does not leave a gap. The form centers itself in the space the
|
|
81
|
+
bar leaves with `margin: auto` on its own block, not `justify-content: center` on
|
|
82
|
+
the column: with the keyboard open the form scrolls from its top instead of
|
|
83
|
+
having its first field pushed out of reach. It sits slightly above the true
|
|
84
|
+
center (`padding-bottom: clamp(0rem, 8dvh, 4rem)`), the optical center of a form.
|
|
85
|
+
|
|
54
86
|
`logo` is a **`ReactNode`** — an inline-SVG brand component, an `<img>`, a
|
|
55
87
|
horizontal lock-up. **The slot fits the artwork, not the reverse:** it caps the
|
|
56
88
|
height (44px in the form header, 2.5rem on the brand panel) and lets the width
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { memo, useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
|
-
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
6
|
-
import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
|
|
7
5
|
import { AUTH } from '../../constants';
|
|
8
6
|
import type { AuthStep } from '../../types';
|
|
9
7
|
|
|
@@ -44,32 +42,12 @@ function AuthContainerRaw({
|
|
|
44
42
|
}
|
|
45
43
|
}, [isEntering]);
|
|
46
44
|
|
|
47
|
-
// Locale picker is mounted as the first child so it sits at the very
|
|
48
|
-
// top of the form column — visually paired with the form chrome (not
|
|
49
|
-
// floating in a viewport corner). Renders automatically when
|
|
50
|
-
// LayoutI18nProvider is mounted (BaseApp does this when the host app
|
|
51
|
-
// passes `i18n`); no prop gating needed.
|
|
52
|
-
const i18n = useLayoutI18nOptional();
|
|
53
|
-
|
|
54
45
|
return (
|
|
55
46
|
<div
|
|
56
47
|
className={`auth-container ${className}`}
|
|
57
48
|
data-entering={isEntering}
|
|
58
49
|
data-step={step}
|
|
59
50
|
>
|
|
60
|
-
{i18n ? (
|
|
61
|
-
<div className="auth-container__meta">
|
|
62
|
-
<LocaleSwitcher
|
|
63
|
-
variant="dropdown"
|
|
64
|
-
buttonVariant="ghost"
|
|
65
|
-
size="sm"
|
|
66
|
-
showFlag
|
|
67
|
-
showCode={false}
|
|
68
|
-
showTriggerLabel
|
|
69
|
-
className="auth-locale-trigger"
|
|
70
|
-
/>
|
|
71
|
-
</div>
|
|
72
|
-
) : null}
|
|
73
51
|
{children}
|
|
74
52
|
</div>
|
|
75
53
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { memo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { ChevronLeft } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { useAppT } from '@djangocfg/i18n';
|
|
8
|
+
import { Link } from '@djangocfg/ui-core/components';
|
|
9
|
+
|
|
10
|
+
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
11
|
+
import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
|
|
12
|
+
import { AUTH } from '../../constants';
|
|
13
|
+
|
|
14
|
+
export interface AuthTopBarProps {
|
|
15
|
+
/**
|
|
16
|
+
* The brand already sits on the media half, which links home itself, so the
|
|
17
|
+
* bar stands its own link down where that half is visible (>= 1024px). CSS
|
|
18
|
+
* owns the breakpoint — see `.auth-topbar` in `auth.css`.
|
|
19
|
+
*/
|
|
20
|
+
brandPanel?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* AuthTopBar — the one place a sign-in screen keeps its way out and its language.
|
|
26
|
+
*
|
|
27
|
+
* Home on the left, locale on the right, pinned to the top of the viewport and
|
|
28
|
+
* independent of what the app passes. Both used to ride inside the centered
|
|
29
|
+
* form stack: the locale floated above the title, and the way out existed only
|
|
30
|
+
* as the `logo` — an optional prop, hidden with the media half on a phone. An
|
|
31
|
+
* app that passed a custom `sidebar` and no `logo` (the Kino sign-in) left a
|
|
32
|
+
* phone visitor with no route off the screen at all.
|
|
33
|
+
*
|
|
34
|
+
* The link is a real `<a>` to `AUTH.HOME_URL` with a visible label, the way an
|
|
35
|
+
* iOS back button names its destination: an arrow alone reads as browser Back,
|
|
36
|
+
* and Back has nowhere to go when the page was opened by a direct link.
|
|
37
|
+
*/
|
|
38
|
+
function AuthTopBarRaw({ brandPanel = false, className = '' }: AuthTopBarProps) {
|
|
39
|
+
const t = useAppT();
|
|
40
|
+
const i18n = useLayoutI18nOptional();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`auth-topbar ${className}`} data-brand-panel={brandPanel}>
|
|
44
|
+
<Link href={AUTH.HOME_URL} className="auth-topbar__home">
|
|
45
|
+
<ChevronLeft aria-hidden="true" className="auth-topbar__icon" />
|
|
46
|
+
<span>{t('layouts.navigation.home')}</span>
|
|
47
|
+
</Link>
|
|
48
|
+
{/* Mounted whenever LayoutI18nProvider is — BaseApp does it when the host
|
|
49
|
+
passes `i18n` — so there is no prop to gate it on. */}
|
|
50
|
+
{i18n ? (
|
|
51
|
+
<LocaleSwitcher
|
|
52
|
+
variant="dropdown"
|
|
53
|
+
buttonVariant="ghost"
|
|
54
|
+
size="sm"
|
|
55
|
+
showFlag
|
|
56
|
+
showCode={false}
|
|
57
|
+
showTriggerLabel
|
|
58
|
+
className="auth-locale-trigger"
|
|
59
|
+
/>
|
|
60
|
+
) : null}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const AuthTopBar = memo(AuthTopBarRaw);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AuthBrandPanel } from './AuthBrandPanel';
|
|
2
2
|
export { AuthContainer } from './AuthContainer';
|
|
3
|
+
export { AuthTopBar } from './AuthTopBar';
|
|
3
4
|
export { AuthHeader } from './AuthHeader';
|
|
4
5
|
export { AuthFooter } from './AuthFooter';
|
|
5
6
|
export { AuthDivider } from './AuthDivider';
|
|
@@ -12,6 +13,7 @@ export { AuthConsent } from './AuthConsent';
|
|
|
12
13
|
|
|
13
14
|
export type { AuthBrandPanelProps } from './AuthBrandPanel';
|
|
14
15
|
export type { AuthContainerProps } from './AuthContainer';
|
|
16
|
+
export type { AuthTopBarProps } from './AuthTopBar';
|
|
15
17
|
export type { AuthHeaderProps } from './AuthHeader';
|
|
16
18
|
export type { AuthFooterProps } from './AuthFooter';
|
|
17
19
|
export type { AuthDividerProps } from './AuthDivider';
|
|
@@ -31,6 +31,7 @@ function AuthShellRaw({
|
|
|
31
31
|
mediaSide = 'left',
|
|
32
32
|
background,
|
|
33
33
|
sidebar,
|
|
34
|
+
topBar,
|
|
34
35
|
className,
|
|
35
36
|
}: AuthShellProps) {
|
|
36
37
|
const { isLoaded: bgLoaded, hasError: bgError } = useImageLoader(background?.imageUrl);
|
|
@@ -73,6 +74,7 @@ function AuthShellRaw({
|
|
|
73
74
|
|
|
74
75
|
const commonShellProps = {
|
|
75
76
|
children,
|
|
77
|
+
topBar,
|
|
76
78
|
className,
|
|
77
79
|
bgStyle,
|
|
78
80
|
overlayStyle,
|
|
@@ -13,6 +13,7 @@ import type { ShellRenderProps } from './types';
|
|
|
13
13
|
*/
|
|
14
14
|
function CenteredShellRaw({
|
|
15
15
|
children,
|
|
16
|
+
topBar,
|
|
16
17
|
className,
|
|
17
18
|
bgStyle,
|
|
18
19
|
overlayStyle,
|
|
@@ -30,6 +31,8 @@ function CenteredShellRaw({
|
|
|
30
31
|
</>
|
|
31
32
|
)}
|
|
32
33
|
|
|
34
|
+
{topBar && <div className="auth-shell-centered__topbar">{topBar}</div>}
|
|
35
|
+
|
|
33
36
|
<div className="auth-shell-centered__content">
|
|
34
37
|
{children}
|
|
35
38
|
</div>
|
|
@@ -25,6 +25,7 @@ interface FullSplitShellProps extends ShellRenderProps {
|
|
|
25
25
|
*/
|
|
26
26
|
function FullSplitShellRaw({
|
|
27
27
|
children,
|
|
28
|
+
topBar,
|
|
28
29
|
className,
|
|
29
30
|
bgStyle,
|
|
30
31
|
overlayStyle,
|
|
@@ -64,6 +65,7 @@ function FullSplitShellRaw({
|
|
|
64
65
|
|
|
65
66
|
{/* Right: solid panel with the centered form */}
|
|
66
67
|
<div className="auth-shell-fullsplit__form">
|
|
68
|
+
{topBar}
|
|
67
69
|
<div className="auth-shell-fullsplit__form-inner">{children}</div>
|
|
68
70
|
</div>
|
|
69
71
|
</div>
|
|
@@ -23,6 +23,7 @@ interface SplitShellProps extends ShellRenderProps {
|
|
|
23
23
|
*/
|
|
24
24
|
function SplitShellRaw({
|
|
25
25
|
children,
|
|
26
|
+
topBar,
|
|
26
27
|
className,
|
|
27
28
|
bgStyle,
|
|
28
29
|
overlayStyle,
|
|
@@ -50,6 +51,7 @@ function SplitShellRaw({
|
|
|
50
51
|
<div className="auth-shell-split__card">
|
|
51
52
|
{/* Left: form */}
|
|
52
53
|
<div className="auth-shell-split__form">
|
|
54
|
+
{topBar}
|
|
53
55
|
<div className="auth-shell-split__form-inner">
|
|
54
56
|
{children}
|
|
55
57
|
</div>
|
|
@@ -50,12 +50,20 @@ export interface AuthShellProps {
|
|
|
50
50
|
background?: AuthBackgroundConfig;
|
|
51
51
|
/** Slot for the right column in split variant (testimonial, branding, etc.). */
|
|
52
52
|
sidebar?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Pinned to the top of the form column, above the vertically centered form:
|
|
55
|
+
* the way out and the locale (see `AuthTopBar`). Optional so a harness that
|
|
56
|
+
* mounts a shell alone still renders.
|
|
57
|
+
*/
|
|
58
|
+
topBar?: ReactNode;
|
|
53
59
|
className?: string;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/** Internal props passed from AuthShell orchestrator to variant shells. */
|
|
57
63
|
export interface ShellRenderProps {
|
|
58
64
|
children: ReactNode;
|
|
65
|
+
/** See `AuthShellProps.topBar`. */
|
|
66
|
+
topBar?: ReactNode;
|
|
59
67
|
className?: string;
|
|
60
68
|
bgStyle?: CSSProperties;
|
|
61
69
|
overlayStyle?: CSSProperties;
|
|
@@ -32,22 +32,65 @@
|
|
|
32
32
|
gap: var(--auth-gap);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.auth-container__meta {
|
|
35
|
+
/* Top bar: the way out (left) and the locale (right), pinned to the top of the
|
|
36
|
+
form column by the shell. The locale used to ride above the title inside the
|
|
37
|
+
centered stack; it is a utility, so it lives on the screen's edge, not in the
|
|
38
|
+
reading order. Contrast stays at `--muted-foreground`, the dimmest token that
|
|
39
|
+
clears AA: fading it further fails the contract gate. */
|
|
40
|
+
.auth-topbar {
|
|
42
41
|
display: flex;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
gap: 0.5rem;
|
|
45
|
+
width: 100%;
|
|
46
|
+
/* A touch target, not a text row: 44px is the floor for a thumb. */
|
|
47
|
+
min-height: 2.75rem;
|
|
48
|
+
/* Clear the notch / status bar when the page runs edge to edge. */
|
|
49
|
+
padding-top: env(safe-area-inset-top, 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.auth-topbar__home {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.125rem;
|
|
56
|
+
min-height: 2.75rem;
|
|
57
|
+
/* Pull the chevron onto the column's left edge; the padding stays as hit area. */
|
|
58
|
+
margin-inline-start: -0.5rem;
|
|
59
|
+
padding-inline: 0.5rem 0.75rem;
|
|
60
|
+
border-radius: var(--auth-radius-sm);
|
|
61
|
+
font-size: 0.875rem;
|
|
62
|
+
color: var(--muted-foreground);
|
|
63
|
+
transition: color 0.15s var(--spring-snappy);
|
|
64
|
+
}
|
|
65
|
+
.auth-topbar__home:hover {
|
|
66
|
+
color: var(--foreground);
|
|
67
|
+
}
|
|
68
|
+
.auth-topbar__home:focus-visible {
|
|
69
|
+
outline: 2px solid var(--ring);
|
|
70
|
+
outline-offset: 2px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.auth-topbar__icon {
|
|
74
|
+
width: 1.25rem;
|
|
75
|
+
height: 1.25rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The locale stays at the right edge even when the link is hidden. */
|
|
79
|
+
.auth-topbar > :last-child {
|
|
80
|
+
margin-inline-start: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* The brand panel on the media half already links home, and that half exists
|
|
84
|
+
from 1024px — the same breakpoint `fullsplit-shell.css` reveals it at. */
|
|
85
|
+
@media (min-width: 1024px) {
|
|
86
|
+
.auth-topbar[data-brand-panel="true"] .auth-topbar__home {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
.auth-locale-trigger {
|
|
49
|
-
height:
|
|
50
|
-
padding-inline: 0.
|
|
92
|
+
height: 2.75rem;
|
|
93
|
+
padding-inline: 0.75rem;
|
|
51
94
|
font-size: 0.75rem;
|
|
52
95
|
color: var(--muted-foreground);
|
|
53
96
|
transition: color 0.15s var(--spring-snappy);
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
display: flex;
|
|
13
13
|
flex-direction: column;
|
|
14
14
|
align-items: center;
|
|
15
|
-
|
|
15
|
+
/* The top bar pins to the top; the content centers itself in what is left
|
|
16
|
+
(see `__content`), so a short viewport never pushes the form out of reach. */
|
|
16
17
|
/* Responsive padding: 24px mobile → 40px desktop */
|
|
17
|
-
padding:
|
|
18
|
+
padding: 0.5rem clamp(1.5rem, 5vw, 2.5rem) 1.5rem;
|
|
18
19
|
padding-bottom: max(1.5rem, env(safe-area-inset-bottom, 1rem));
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -39,11 +40,19 @@
|
|
|
39
40
|
pointer-events: none;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
.auth-shell-centered__topbar {
|
|
44
|
+
position: relative;
|
|
45
|
+
z-index: 1;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
/* Content sits above background */
|
|
43
50
|
.auth-shell-centered__content {
|
|
44
51
|
position: relative;
|
|
45
52
|
z-index: 1;
|
|
46
53
|
width: 100%;
|
|
54
|
+
margin-block: auto;
|
|
55
|
+
padding-bottom: clamp(0rem, 8dvh, 4rem);
|
|
47
56
|
display: flex;
|
|
48
57
|
flex-direction: column;
|
|
49
58
|
align-items: center;
|
|
@@ -147,18 +147,26 @@ a.auth-brand-panel__mark:focus-visible {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/* ── Form half (right) ─────────────────────────────────────────────────── */
|
|
150
|
+
/* A column: the top bar sits at the top of the viewport, and the form takes the
|
|
151
|
+
rest and centers itself in it. `margin: auto` on the inner block (rather than
|
|
152
|
+
`justify-content: center` on the column) is what keeps the form reachable when
|
|
153
|
+
the keyboard or a short viewport leaves it less room than it needs — centering
|
|
154
|
+
by the container would push the top of it out of reach. */
|
|
150
155
|
.auth-shell-fullsplit__form {
|
|
151
156
|
flex: 1;
|
|
152
157
|
display: flex;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
padding: 1.5rem clamp(1.5rem, 5vw, 2.5rem);
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
padding: 0.5rem clamp(1.5rem, 5vw, 2.5rem);
|
|
156
160
|
padding-bottom: max(1.5rem, env(safe-area-inset-bottom, 1rem));
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
.auth-shell-fullsplit__form-inner {
|
|
160
164
|
width: 100%;
|
|
161
165
|
max-width: 360px;
|
|
166
|
+
margin: auto;
|
|
167
|
+
/* Sits a little above the true center — the optical center of a form — and
|
|
168
|
+
the top bar's height keeps it from looking low. */
|
|
169
|
+
padding-bottom: clamp(0rem, 8dvh, 4rem);
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
/* ── Desktop: reveal the image half, split 50/50 edge-to-edge ──────────── */
|
|
@@ -215,7 +223,7 @@ a.auth-brand-panel__mark:focus-visible {
|
|
|
215
223
|
|
|
216
224
|
.auth-shell-fullsplit__form {
|
|
217
225
|
flex: 1;
|
|
218
|
-
padding: 2.5rem;
|
|
226
|
+
padding: 1.5rem 2.5rem 2.5rem;
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
.auth-shell-fullsplit__form-inner {
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
min-height: 100vh;
|
|
14
14
|
min-height: 100dvh;
|
|
15
15
|
display: flex;
|
|
16
|
-
|
|
16
|
+
/* Mobile: the card fills the viewport so the top bar lands at its top edge;
|
|
17
|
+
desktop re-centers the floating card below. */
|
|
18
|
+
align-items: stretch;
|
|
17
19
|
justify-content: center;
|
|
18
|
-
padding: 1rem;
|
|
20
|
+
padding: 0.5rem 1rem 1rem;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/* Background layers — hidden on mobile (plain form), shown on desktop. */
|
|
@@ -47,14 +49,15 @@
|
|
|
47
49
|
.auth-shell-split__form {
|
|
48
50
|
flex: 1;
|
|
49
51
|
display: flex;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
padding: 1rem 0.5rem;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
padding: 0 0.5rem 1rem;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
.auth-shell-split__form-inner {
|
|
56
57
|
width: 100%;
|
|
57
58
|
max-width: 360px;
|
|
59
|
+
/* Centers in what the top bar leaves, and stays reachable when it is short. */
|
|
60
|
+
margin: auto;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/* Sidebar column — hidden on mobile, shown on desktop. */
|
|
@@ -64,6 +67,11 @@
|
|
|
64
67
|
|
|
65
68
|
/* ── Desktop: frosted-glass two-column card ──────────────────────────────── */
|
|
66
69
|
@media (min-width: 1280px) {
|
|
70
|
+
.auth-shell-split {
|
|
71
|
+
align-items: center;
|
|
72
|
+
padding: 1rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
.auth-shell-split__bg,
|
|
68
76
|
.auth-shell-split__overlay {
|
|
69
77
|
display: block;
|
|
@@ -91,7 +99,7 @@
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
.auth-shell-split__form {
|
|
94
|
-
padding: 2.75rem;
|
|
102
|
+
padding: 1.25rem 2.75rem 2.75rem;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
.auth-shell-split__sidebar {
|
package/src/testing/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export { AuthLayoutProvider, AuthSuccessScreen } from '../layouts/AuthLayout/Aut
|
|
|
27
27
|
// The layout's own default media-half content. Stories mount `AuthShell`
|
|
28
28
|
// directly, so they must supply this themselves to preview what an app that
|
|
29
29
|
// passes no `sidebar` actually gets.
|
|
30
|
-
export { AuthBrandPanel } from '../layouts/AuthLayout/components/shared';
|
|
30
|
+
export { AuthBrandPanel, AuthTopBar } from '../layouts/AuthLayout/components/shared';
|
|
31
31
|
export {
|
|
32
32
|
IdentifierStep,
|
|
33
33
|
OTPStep,
|