@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.608",
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.608",
95
- "@djangocfg/api": "^2.1.608",
96
- "@djangocfg/devtools": "^2.1.608",
97
- "@djangocfg/i18n": "^2.1.608",
98
- "@djangocfg/ui-core": "^2.1.608",
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.608",
129
- "@djangocfg/api": "^2.1.608",
130
- "@djangocfg/devtools": "^2.1.608",
131
- "@djangocfg/i18n": "^2.1.608",
132
- "@djangocfg/typescript-config": "^2.1.608",
133
- "@djangocfg/ui-core": "^2.1.608",
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-svh flex-col items-center justify-center gap-4 p-6 text-center"
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-svh + overflow-hidden: lock the shell to exactly one viewport height so
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-svh overflow-hidden';
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. */