@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.569",
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.569",
94
- "@djangocfg/api": "^2.1.569",
95
- "@djangocfg/devtools": "^2.1.569",
96
- "@djangocfg/i18n": "^2.1.569",
97
- "@djangocfg/ui-core": "^2.1.569",
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.569",
128
- "@djangocfg/api": "^2.1.569",
129
- "@djangocfg/devtools": "^2.1.569",
130
- "@djangocfg/i18n": "^2.1.569",
131
- "@djangocfg/typescript-config": "^2.1.569",
132
- "@djangocfg/ui-core": "^2.1.569",
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
- <span className="auth-brand-panel__mark">{logo}</span>
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
- {logo && (
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,
@@ -39,6 +39,7 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
39
39
  const requireTermsAcceptance = Boolean(termsUrl || privacyUrl);
40
40
 
41
41
  // Use the auth form hook
42
+
42
43
  const authForm = useAuthForm({
43
44
  onIdentifierSuccess,
44
45
  onOTPSuccess,
@@ -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