@djangocfg/layouts 2.1.569 → 2.1.570
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/README.md +11 -0
- package/src/layouts/AuthLayout/components/shared/AuthBrandPanel.tsx +27 -1
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +24 -5
- package/src/layouts/AuthLayout/constants.ts +13 -0
- package/src/layouts/AuthLayout/context.tsx +1 -0
- package/src/layouts/AuthLayout/styles/auth.css +17 -0
- package/src/layouts/AuthLayout/styles/fullsplit-shell.css +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.570",
|
|
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.570",
|
|
94
|
+
"@djangocfg/api": "^2.1.570",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.570",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.570",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.570",
|
|
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.570",
|
|
128
|
+
"@djangocfg/api": "^2.1.570",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.570",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.570",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.570",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.570",
|
|
133
133
|
"@storybook/react-vite": "^10.5.0",
|
|
134
134
|
"@types/node": "^25.9.5",
|
|
135
135
|
"@types/react": "19.2.15",
|
|
@@ -40,6 +40,17 @@ the only brand left and it returns. CSS owns that half of the rule
|
|
|
40
40
|
(`.auth-header[data-brand-elsewhere]`) — a JS width branch would need a
|
|
41
41
|
viewport the server does not have and would flicker on hydration.
|
|
42
42
|
|
|
43
|
+
**The mark is a link to `/`** — the way out. A sign-in screen is otherwise a
|
|
44
|
+
dead end: arrive by a direct link, or get bounced here by an expired session,
|
|
45
|
+
and there is no route back to the site (Back has nowhere to return to either).
|
|
46
|
+
There is no prop for it: every app has a root, and a localized app resolves `/`
|
|
47
|
+
through next-intl's own `Link`, so `/ru/auth` returns to `/ru`. The href lives
|
|
48
|
+
in `constants.ts` as `AUTH.HOME_URL`.
|
|
49
|
+
|
|
50
|
+
The link carries `aria-label` from `layouts.navigation.home` rather than
|
|
51
|
+
relying on its contents: `logo` is an opaque node, and a brand that is a bare
|
|
52
|
+
SVG would otherwise announce as an unnamed link.
|
|
53
|
+
|
|
43
54
|
`logo` is a **`ReactNode`** — an inline-SVG brand component, an `<img>`, a
|
|
44
55
|
horizontal lock-up. **The slot fits the artwork, not the reverse:** it caps the
|
|
45
56
|
height (44px in the form header, 2.5rem on the brand panel) and lets the width
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { memo } from 'react';
|
|
4
4
|
|
|
5
|
+
import { useAppT } from '@djangocfg/i18n';
|
|
6
|
+
import { Link } from '@djangocfg/ui-core/components';
|
|
7
|
+
|
|
8
|
+
import { AUTH } from '../../constants';
|
|
9
|
+
|
|
5
10
|
export interface AuthBrandPanelProps {
|
|
6
11
|
/** The brand node the app already passes to `AuthLayout` as `logo`. */
|
|
7
12
|
logo: React.ReactNode;
|
|
@@ -31,11 +36,32 @@ export interface AuthBrandPanelProps {
|
|
|
31
36
|
*
|
|
32
37
|
* Text is light because it sits on the scrim `fullsplit-shell.css` paints over
|
|
33
38
|
* the photo, not on a theme surface — so it does not follow `--foreground`.
|
|
39
|
+
*
|
|
40
|
+
* The mark is the way OUT. A sign-in screen is otherwise a dead end: arrive by
|
|
41
|
+
* a direct link or get bounced here by an expired session and there is no
|
|
42
|
+
* route back to the site — Back has nowhere to go either. A brand mark in the
|
|
43
|
+
* corner is where people already click to leave, so it is the control to make
|
|
44
|
+
* real rather than a second one to add. It always links to `AUTH.HOME_URL`;
|
|
45
|
+
* nothing to configure.
|
|
34
46
|
*/
|
|
35
47
|
function AuthBrandPanelRaw({ logo, tagline, className = '' }: AuthBrandPanelProps) {
|
|
48
|
+
/*
|
|
49
|
+
* The label is read from the shared navigation catalogue rather than taken
|
|
50
|
+
* as a prop: it is the same word in all 17 locales, and a `logo` is an
|
|
51
|
+
* opaque node this component cannot read a name out of — a brand that is a
|
|
52
|
+
* bare SVG would otherwise announce as an unnamed link.
|
|
53
|
+
*/
|
|
54
|
+
const t = useAppT();
|
|
55
|
+
|
|
36
56
|
return (
|
|
37
57
|
<div className={`auth-brand-panel ${className}`}>
|
|
38
|
-
<
|
|
58
|
+
<Link
|
|
59
|
+
href={AUTH.HOME_URL}
|
|
60
|
+
className="auth-brand-panel__mark"
|
|
61
|
+
aria-label={t('layouts.navigation.home')}
|
|
62
|
+
>
|
|
63
|
+
{logo}
|
|
64
|
+
</Link>
|
|
39
65
|
{tagline && <p className="auth-brand-panel__tagline">{tagline}</p>}
|
|
40
66
|
</div>
|
|
41
67
|
);
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { memo } from 'react';
|
|
4
4
|
|
|
5
|
+
import { useAppT } from '@djangocfg/i18n';
|
|
6
|
+
import { Link } from '@djangocfg/ui-core/components';
|
|
7
|
+
|
|
8
|
+
import { AUTH } from '../../constants';
|
|
9
|
+
|
|
5
10
|
export interface AuthHeaderProps {
|
|
6
11
|
logo?: React.ReactNode;
|
|
7
12
|
title: string;
|
|
@@ -34,13 +39,27 @@ function AuthHeaderRaw({
|
|
|
34
39
|
brandElsewhere = false,
|
|
35
40
|
className = '',
|
|
36
41
|
}: AuthHeaderProps) {
|
|
42
|
+
const t = useAppT();
|
|
43
|
+
/*
|
|
44
|
+
* A link, not decoration — so no `aria-hidden`: it is the one way off this
|
|
45
|
+
* screen and must be reachable by keyboard and named for a screen reader.
|
|
46
|
+
* The name comes from the shared catalogue because `logo` is an opaque node
|
|
47
|
+
* this component cannot read a name out of; a brand that is a bare SVG
|
|
48
|
+
* would otherwise announce as an unnamed link.
|
|
49
|
+
*/
|
|
50
|
+
const mark = logo ? (
|
|
51
|
+
<Link
|
|
52
|
+
href={AUTH.HOME_URL}
|
|
53
|
+
className="auth-logo"
|
|
54
|
+
aria-label={t('layouts.navigation.home')}
|
|
55
|
+
>
|
|
56
|
+
{logo}
|
|
57
|
+
</Link>
|
|
58
|
+
) : null;
|
|
59
|
+
|
|
37
60
|
return (
|
|
38
61
|
<div className={`auth-header ${className}`} data-brand-elsewhere={brandElsewhere}>
|
|
39
|
-
{
|
|
40
|
-
<span className="auth-logo" aria-hidden="true">
|
|
41
|
-
{logo}
|
|
42
|
-
</span>
|
|
43
|
-
)}
|
|
62
|
+
{mark}
|
|
44
63
|
<h1 className="auth-title">{title}</h1>
|
|
45
64
|
{subtitle && (
|
|
46
65
|
<p className="auth-subtitle" aria-live="polite">
|
|
@@ -16,6 +16,19 @@ export const AUTH = {
|
|
|
16
16
|
BACKUP_CODE_MAX_LENGTH: AUTH_CONSTANTS.BACKUP_CODE_MAX_LENGTH,
|
|
17
17
|
|
|
18
18
|
// Timing (ms)
|
|
19
|
+
/**
|
|
20
|
+
* Where the brand mark links to — the way OUT of the sign-in screen.
|
|
21
|
+
*
|
|
22
|
+
* Sign-in is otherwise a dead end: a visitor who arrived by a direct link,
|
|
23
|
+
* or who was bounced here by an expired session, has no in-page route back
|
|
24
|
+
* and the browser's Back button has nowhere to return to.
|
|
25
|
+
*
|
|
26
|
+
* Not configurable, and the site ROOT rather than `redirectUrl` (which is
|
|
27
|
+
* where a *successful* sign-in lands — often a private page that would
|
|
28
|
+
* bounce a signed-out visitor straight back here). A localized app resolves
|
|
29
|
+
* this through next-intl's `Link`, so `/ru/auth` returns to `/ru`.
|
|
30
|
+
*/
|
|
31
|
+
HOME_URL: '/',
|
|
19
32
|
ANIMATION_DURATION: 400,
|
|
20
33
|
AUTO_SUBMIT_DELAY: 100,
|
|
21
34
|
COPY_FEEDBACK_DURATION: 2000,
|
|
@@ -116,6 +116,23 @@
|
|
|
116
116
|
object-fit: contain;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/* As a link home it is a control, so it needs a visible focus ring and a
|
|
120
|
+
pointer state; as a decorative mark it stays inert. */
|
|
121
|
+
a.auth-logo {
|
|
122
|
+
color: inherit;
|
|
123
|
+
border-radius: var(--auth-radius-sm);
|
|
124
|
+
transition: opacity 0.15s var(--spring-snappy);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
a.auth-logo:hover {
|
|
128
|
+
opacity: 0.82;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
a.auth-logo:focus-visible {
|
|
132
|
+
outline: 2px solid var(--ring);
|
|
133
|
+
outline-offset: 4px;
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
@keyframes authLogoIn {
|
|
120
137
|
from {
|
|
121
138
|
opacity: 0;
|
|
@@ -110,6 +110,23 @@
|
|
|
110
110
|
align-items: center;
|
|
111
111
|
height: 2.5rem;
|
|
112
112
|
max-width: 100%;
|
|
113
|
+
/* `align-self` keeps the hit area the width of the artwork: the panel is a
|
|
114
|
+
flex column, so a link would otherwise stretch the full column width and
|
|
115
|
+
swallow clicks well past the mark. */
|
|
116
|
+
align-self: flex-start;
|
|
117
|
+
color: inherit;
|
|
118
|
+
border-radius: var(--auth-radius-sm);
|
|
119
|
+
transition: opacity 0.15s var(--spring-snappy);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Only the link form reacts — a plain mark is not a control. */
|
|
123
|
+
a.auth-brand-panel__mark:hover {
|
|
124
|
+
opacity: 0.82;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
a.auth-brand-panel__mark:focus-visible {
|
|
128
|
+
outline: 2px solid #fff;
|
|
129
|
+
outline-offset: 6px;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
/* Same contract as `.auth-logo`: cap the height, let the width follow the
|