@djangocfg/layouts 2.1.608 → 2.1.610
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/AppLayout/BaseApp.tsx +5 -0
- package/src/layouts/AppLayout/pwaMeta.test.ts +36 -0
- package/src/layouts/AppLayout/pwaMeta.tsx +40 -0
- package/src/layouts/PrivateLayout/components/ForbiddenState.tsx +1 -1
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +2 -2
- package/src/layouts/types/layout.types.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.610",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -91,11 +91,11 @@
|
|
|
91
91
|
"test": "vitest run"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@djangocfg/analytics": "^2.1.
|
|
95
|
-
"@djangocfg/api": "^2.1.
|
|
96
|
-
"@djangocfg/devtools": "^2.1.
|
|
97
|
-
"@djangocfg/i18n": "^2.1.
|
|
98
|
-
"@djangocfg/ui-core": "^2.1.
|
|
94
|
+
"@djangocfg/analytics": "^2.1.610",
|
|
95
|
+
"@djangocfg/api": "^2.1.610",
|
|
96
|
+
"@djangocfg/devtools": "^2.1.610",
|
|
97
|
+
"@djangocfg/i18n": "^2.1.610",
|
|
98
|
+
"@djangocfg/ui-core": "^2.1.610",
|
|
99
99
|
"@hookform/resolvers": "^5.2.2",
|
|
100
100
|
"consola": "^3.4.2",
|
|
101
101
|
"lucide-react": "^0.545.0",
|
|
@@ -125,12 +125,12 @@
|
|
|
125
125
|
"uuid": "^11.1.1"
|
|
126
126
|
},
|
|
127
127
|
"devDependencies": {
|
|
128
|
-
"@djangocfg/analytics": "^2.1.
|
|
129
|
-
"@djangocfg/api": "^2.1.
|
|
130
|
-
"@djangocfg/devtools": "^2.1.
|
|
131
|
-
"@djangocfg/i18n": "^2.1.
|
|
132
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
133
|
-
"@djangocfg/ui-core": "^2.1.
|
|
128
|
+
"@djangocfg/analytics": "^2.1.610",
|
|
129
|
+
"@djangocfg/api": "^2.1.610",
|
|
130
|
+
"@djangocfg/devtools": "^2.1.610",
|
|
131
|
+
"@djangocfg/i18n": "^2.1.610",
|
|
132
|
+
"@djangocfg/typescript-config": "^2.1.610",
|
|
133
|
+
"@djangocfg/ui-core": "^2.1.610",
|
|
134
134
|
"@storybook/react-vite": "^10.5.0",
|
|
135
135
|
"@types/node": "^25.9.5",
|
|
136
136
|
"@types/react": "19.2.15",
|
|
@@ -62,6 +62,7 @@ import { AuthDialog } from '../../snippets/AuthDialog';
|
|
|
62
62
|
import type { BaseLayoutProps } from '../types/layout.types';
|
|
63
63
|
import { LayoutI18nProvider } from '../../shared/LayoutI18nProvider';
|
|
64
64
|
import { ProjectNameProvider } from '../../shared/ProjectContext';
|
|
65
|
+
import { PwaMetaGuard } from './pwaMeta';
|
|
65
66
|
|
|
66
67
|
import React from 'react';
|
|
67
68
|
|
|
@@ -105,6 +106,7 @@ export function BaseApp({
|
|
|
105
106
|
swr,
|
|
106
107
|
devtools: devtoolsConfig,
|
|
107
108
|
i18n,
|
|
109
|
+
pwaMeta = true,
|
|
108
110
|
}: BaseAppProps) {
|
|
109
111
|
// auth=false disables AuthProvider entirely (public apps with no login)
|
|
110
112
|
const authEnabled = auth !== false;
|
|
@@ -122,6 +124,8 @@ export function BaseApp({
|
|
|
122
124
|
};
|
|
123
125
|
|
|
124
126
|
const content = (
|
|
127
|
+
<>
|
|
128
|
+
{pwaMeta && <PwaMetaGuard />}
|
|
125
129
|
<ThemeProvider
|
|
126
130
|
defaultTheme={theme?.defaultTheme || 'system'}
|
|
127
131
|
storageKey={theme?.storageKey}
|
|
@@ -192,6 +196,7 @@ export function BaseApp({
|
|
|
192
196
|
</NextLinkProvider>
|
|
193
197
|
</NextRouterAdapter>
|
|
194
198
|
</ThemeProvider>
|
|
199
|
+
</>
|
|
195
200
|
);
|
|
196
201
|
|
|
197
202
|
// The error boundary is now UiProviders' built-in one (fed our i18n fallback
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PWA_META_SCRIPT } from './pwaMeta';
|
|
5
|
+
|
|
6
|
+
const meta = (n: string) => document.head.querySelector(`meta[name="${n}"]`)?.getAttribute('content');
|
|
7
|
+
const run = () => new Function(PWA_META_SCRIPT)();
|
|
8
|
+
|
|
9
|
+
describe('PWA_META_SCRIPT', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
document.head.innerHTML = '';
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('adds viewport-fit=cover to an existing viewport and the iOS app tags', () => {
|
|
15
|
+
document.head.innerHTML = '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
|
16
|
+
run();
|
|
17
|
+
expect(meta('viewport')).toBe('width=device-width, initial-scale=1, viewport-fit=cover');
|
|
18
|
+
expect(meta('apple-mobile-web-app-capable')).toBe('yes');
|
|
19
|
+
expect(meta('mobile-web-app-capable')).toBe('yes');
|
|
20
|
+
expect(meta('apple-mobile-web-app-status-bar-style')).toBe('black-translucent');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('creates a viewport when none exists', () => {
|
|
24
|
+
run();
|
|
25
|
+
expect(meta('viewport')).toContain('viewport-fit=cover');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('never overrides what the app set', () => {
|
|
29
|
+
document.head.innerHTML =
|
|
30
|
+
'<meta name="viewport" content="width=device-width, viewport-fit=contain">' +
|
|
31
|
+
'<meta name="apple-mobile-web-app-status-bar-style" content="default">';
|
|
32
|
+
run();
|
|
33
|
+
expect(meta('viewport')).toBe('width=device-width, viewport-fit=contain');
|
|
34
|
+
expect(meta('apple-mobile-web-app-status-bar-style')).toBe('default');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed-app `<meta>` tags, applied by `BaseApp` so an app does not have to remember them.
|
|
3
|
+
*
|
|
4
|
+
* Next's App Router builds `<head>` from the server `viewport` / `metadata` exports; a client component such as
|
|
5
|
+
* `BaseApp` cannot contribute to them, and a second `<meta name="viewport">` would fight Next's own. So this is a tiny
|
|
6
|
+
* inline script (it runs while the HTML is parsed, before first paint) that completes what is missing in `<head>`:
|
|
7
|
+
*
|
|
8
|
+
* viewport + `viewport-fit=cover` (without it every `--safe-*` inset is 0 and the
|
|
9
|
+
* installed iOS app letterboxes)
|
|
10
|
+
* apple-mobile-web-app-capable yes (Next 16 only emits `mobile-web-app-capable`; iOS < 26 needs the apple tag)
|
|
11
|
+
* mobile-web-app-capable yes
|
|
12
|
+
* apple-mobile-web-app-status-bar-style black-translucent
|
|
13
|
+
*
|
|
14
|
+
* Existing tags are never overridden (an app's own `appleWebApp.statusBarStyle` wins); only `viewport-fit` is added to
|
|
15
|
+
* an existing viewport that lacks it. A MutationObserver re-applies it if a client navigation rewrites the viewport tag.
|
|
16
|
+
* Prefer exporting `viewport = createViewport(...)` (default `viewportFit: 'cover'`): that puts it in the server HTML;
|
|
17
|
+
* this is the safety net for apps that forgot.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as React from 'react';
|
|
21
|
+
|
|
22
|
+
export const PWA_META_SCRIPT = `(function(){try{
|
|
23
|
+
var d=document,h=d.head;if(!h)return;
|
|
24
|
+
function q(n){return h.querySelector('meta[name="'+n+'"]')}
|
|
25
|
+
function ensure(n,c){if(!q(n)){var m=d.createElement('meta');m.name=n;m.content=c;h.appendChild(m)}}
|
|
26
|
+
function fit(){var v=q('viewport');
|
|
27
|
+
if(!v){v=d.createElement('meta');v.name='viewport';v.content='width=device-width, initial-scale=1, viewport-fit=cover';h.appendChild(v);return}
|
|
28
|
+
var c=v.getAttribute('content')||'';
|
|
29
|
+
if(!/viewport-fit\\s*=/.test(c))v.setAttribute('content',(c?c+', ':'')+'viewport-fit=cover')}
|
|
30
|
+
fit();
|
|
31
|
+
ensure('apple-mobile-web-app-capable','yes');
|
|
32
|
+
ensure('mobile-web-app-capable','yes');
|
|
33
|
+
ensure('apple-mobile-web-app-status-bar-style','black-translucent');
|
|
34
|
+
if(window.MutationObserver){new MutationObserver(function(){var v=q('viewport');if(!v||!/viewport-fit\\s*=\\s*cover/.test(v.getAttribute('content')||''))fit()}).observe(h,{subtree:true,childList:true,attributes:true,attributeFilter:['content']})}
|
|
35
|
+
}catch(e){}})();`;
|
|
36
|
+
|
|
37
|
+
/** Renders the inline script. Server-rendered into the HTML; React does not re-run it on hydration. */
|
|
38
|
+
export function PwaMetaGuard() {
|
|
39
|
+
return <script dangerouslySetInnerHTML={{ __html: PWA_META_SCRIPT }} suppressHydrationWarning />;
|
|
40
|
+
}
|
|
@@ -36,7 +36,7 @@ export function ForbiddenState({
|
|
|
36
36
|
return (
|
|
37
37
|
<div
|
|
38
38
|
role="alert"
|
|
39
|
-
className="flex min-h-
|
|
39
|
+
className="flex min-h-[var(--app-height,100svh)] flex-col items-center justify-center gap-4 p-6 text-center"
|
|
40
40
|
>
|
|
41
41
|
<div className="flex size-12 items-center justify-center rounded-full bg-destructive/10">
|
|
42
42
|
<ShieldAlert className="size-6 text-destructive" />
|
|
@@ -71,10 +71,10 @@ const BOXED_BG_CLASS: Record<NonNullable<LayoutVisualConfig['background']>, stri
|
|
|
71
71
|
export function resolveProviderClassName(
|
|
72
72
|
visual: LayoutVisualConfig | undefined,
|
|
73
73
|
): string | undefined {
|
|
74
|
-
// h-
|
|
74
|
+
// h-[var(--app-height)] + overflow-hidden: lock the shell to exactly one viewport height so
|
|
75
75
|
// the inner scroll-area (PrivateContent) is the only scroll surface. Without
|
|
76
76
|
// this, SidebarProvider grows via min-h-svh and the whole page scrolls.
|
|
77
|
-
const base = 'h-
|
|
77
|
+
const base = 'h-[var(--app-height,100svh)] overflow-hidden';
|
|
78
78
|
if ((visual?.variant ?? 'full-bleed') !== 'boxed') return base;
|
|
79
79
|
// `max-md:!bg-background` neutralises shadcn-sidebar's built-in
|
|
80
80
|
// `has-[[data-variant=inset]]:bg-sidebar` below md so the mobile Drawer shell
|
|
@@ -72,6 +72,13 @@ export interface BaseLayoutProps {
|
|
|
72
72
|
* UserMenu, LocaleSwitcher) reads from there via `useLayoutI18n()`.
|
|
73
73
|
*/
|
|
74
74
|
i18n?: I18nLayoutConfig;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Installed-app meta tags (`viewport-fit=cover`, `apple-mobile-web-app-capable`, `mobile-web-app-capable`,
|
|
78
|
+
* translucent status bar), added to `<head>` when the app's own `viewport`/`metadata` lack them. On by default;
|
|
79
|
+
* pass `false` to opt out.
|
|
80
|
+
*/
|
|
81
|
+
pwaMeta?: boolean;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
/** Devtools config for BaseApp — the `<Devtools>` props minus children. */
|