@djangocfg/ui-core 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/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.610",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
"check:docs": "node scripts/check-docs.mjs"
|
|
134
134
|
},
|
|
135
135
|
"peerDependencies": {
|
|
136
|
-
"@djangocfg/api": "^2.1.
|
|
137
|
-
"@djangocfg/i18n": "^2.1.
|
|
136
|
+
"@djangocfg/api": "^2.1.610",
|
|
137
|
+
"@djangocfg/i18n": "^2.1.610",
|
|
138
138
|
"consola": "^3.4.2",
|
|
139
139
|
"lucide-react": "^0.545.0",
|
|
140
140
|
"moment": "^2.30.1",
|
|
@@ -210,10 +210,10 @@
|
|
|
210
210
|
"vaul": "1.1.2"
|
|
211
211
|
},
|
|
212
212
|
"devDependencies": {
|
|
213
|
-
"@djangocfg/api": "^2.1.
|
|
214
|
-
"@djangocfg/eslint-config": "^2.1.
|
|
215
|
-
"@djangocfg/i18n": "^2.1.
|
|
216
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
213
|
+
"@djangocfg/api": "^2.1.610",
|
|
214
|
+
"@djangocfg/eslint-config": "^2.1.610",
|
|
215
|
+
"@djangocfg/i18n": "^2.1.610",
|
|
216
|
+
"@djangocfg/typescript-config": "^2.1.610",
|
|
217
217
|
"@storybook/react-vite": "^10.5.0",
|
|
218
218
|
"@types/node": "^24.13.3",
|
|
219
219
|
"@types/react": "19.2.15",
|
|
@@ -131,7 +131,7 @@ const SidebarProvider = React.forwardRef<
|
|
|
131
131
|
} as React.CSSProperties
|
|
132
132
|
}
|
|
133
133
|
className={cn(
|
|
134
|
-
"group/sidebar-wrapper flex min-h-
|
|
134
|
+
"group/sidebar-wrapper flex min-h-[var(--app-height,100svh)] w-full has-[&_[data-variant=inset]]:bg-sidebar",
|
|
135
135
|
className
|
|
136
136
|
)}
|
|
137
137
|
ref={ref}
|
|
@@ -262,7 +262,7 @@ const Sidebar = React.forwardRef<
|
|
|
262
262
|
/>
|
|
263
263
|
<div
|
|
264
264
|
className={cn(
|
|
265
|
-
"fixed inset-y-0 z-10 hidden h-
|
|
265
|
+
"fixed inset-y-0 z-10 hidden h-[var(--app-height,100svh)] transition-[left,right,width] duration-200 ease-linear md:flex",
|
|
266
266
|
side === "left"
|
|
267
267
|
? state === "collapsed" && collapsible === "offcanvas"
|
|
268
268
|
? "left-[calc(var(--sidebar-width)*-1)]"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--app-height`: the real height of the app viewport, kept on `<html>`.
|
|
3
|
+
*
|
|
4
|
+
* Why it is measured: in an installed iOS app with `viewport-fit=cover` + a translucent status bar, WebKit reports
|
|
5
|
+
* `100%`, `100svh` and `100dvh` as the screen height MINUS the top inset, so a shell sized from them stops short and
|
|
6
|
+
* leaves a dead band at the bottom (and `overflow: hidden` clips the rest). `100lvh` and `window.innerHeight` are not
|
|
7
|
+
* always right either, across iOS versions. So in an installed mode we take the largest of the candidates, capped at
|
|
8
|
+
* the physical screen, and publish it as `--app-height`. In a browser tab the property is removed and the CSS default
|
|
9
|
+
* (`100dvh`, see base.css) applies. The shell paints under the status bar and pads by `--safe-top` itself.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { detectDisplayMode, isInstalledMode, subscribeDisplayMode } from './displayMode';
|
|
13
|
+
|
|
14
|
+
const PROP = '--app-height';
|
|
15
|
+
|
|
16
|
+
function probeHeight(unit: 'lvh' | 'dvh'): number {
|
|
17
|
+
if (typeof CSS !== 'undefined' && CSS.supports && !CSS.supports('height', `100${unit}`)) return 0;
|
|
18
|
+
const el = document.createElement('div');
|
|
19
|
+
el.style.cssText = `position:fixed;top:0;left:0;width:0;height:100${unit};visibility:hidden;pointer-events:none`;
|
|
20
|
+
document.body.appendChild(el);
|
|
21
|
+
const h = el.getBoundingClientRect().height;
|
|
22
|
+
el.remove();
|
|
23
|
+
return h;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Largest plausible app height in px, or 0 when nothing could be measured. */
|
|
27
|
+
export function measureAppHeight(): number {
|
|
28
|
+
if (typeof document === 'undefined' || !document.body) return 0;
|
|
29
|
+
const screenMax = Math.max(window.screen?.height || 0, window.screen?.width || 0);
|
|
30
|
+
const candidates = [probeHeight('lvh'), probeHeight('dvh'), window.innerHeight].filter((n) => n > 0);
|
|
31
|
+
if (!candidates.length) return 0;
|
|
32
|
+
const best = Math.max(...candidates);
|
|
33
|
+
return screenMax > 0 ? Math.min(best, screenMax) : best;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Keeps `--app-height` in step with the installed viewport. Returns a cleanup. */
|
|
37
|
+
export function syncAppHeight(): () => void {
|
|
38
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return () => {};
|
|
39
|
+
const root = document.documentElement;
|
|
40
|
+
const timers: number[] = [];
|
|
41
|
+
|
|
42
|
+
const apply = () => {
|
|
43
|
+
if (!isInstalledMode(detectDisplayMode())) {
|
|
44
|
+
root.style.removeProperty(PROP);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const h = measureAppHeight();
|
|
48
|
+
if (h > 0) root.style.setProperty(PROP, `${Math.round(h)}px`);
|
|
49
|
+
};
|
|
50
|
+
// iOS reports the new size a beat after `orientationchange`; re-measure once it settled.
|
|
51
|
+
const applyLater = () => {
|
|
52
|
+
apply();
|
|
53
|
+
timers.push(window.setTimeout(apply, 120), window.setTimeout(apply, 400));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
apply();
|
|
57
|
+
window.addEventListener('resize', apply);
|
|
58
|
+
window.addEventListener('orientationchange', applyLater);
|
|
59
|
+
window.addEventListener('pageshow', apply);
|
|
60
|
+
const unsubscribe = subscribeDisplayMode(apply);
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener('resize', apply);
|
|
63
|
+
window.removeEventListener('orientationchange', applyLater);
|
|
64
|
+
window.removeEventListener('pageshow', apply);
|
|
65
|
+
unsubscribe();
|
|
66
|
+
timers.forEach((t) => window.clearTimeout(t));
|
|
67
|
+
root.style.removeProperty(PROP);
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/hooks/media/index.ts
CHANGED
|
@@ -7,4 +7,5 @@ export { useIsTouch } from './useIsTouch';
|
|
|
7
7
|
export { useDisplayMode, useIsPwa } from './useDisplayMode';
|
|
8
8
|
export { detectDisplayMode, syncDisplayModeAttribute } from './displayMode';
|
|
9
9
|
export type { DisplayMode } from './displayMode';
|
|
10
|
+
export { syncAppHeight, measureAppHeight } from './appHeight';
|
|
10
11
|
export { useBlockGestureZoom } from './useBlockGestureZoom';
|
|
@@ -7,6 +7,7 @@ import { TooltipProvider } from '../components/overlay/tooltip';
|
|
|
7
7
|
import { Toaster } from '../components/feedback/sonner';
|
|
8
8
|
import { Boundary, type BoundaryProps } from '../components/boundary';
|
|
9
9
|
import { syncDisplayModeAttribute } from '../hooks/media/displayMode';
|
|
10
|
+
import { syncAppHeight } from '../hooks/media/appHeight';
|
|
10
11
|
import { installChunkReloadListeners } from '../lib/chunk-reload';
|
|
11
12
|
import { DialogProvider } from '../lib/dialog-service/DialogProvider';
|
|
12
13
|
|
|
@@ -88,6 +89,8 @@ export function UiProviders({
|
|
|
88
89
|
React.useEffect(() => installChunkReloadListeners(), []);
|
|
89
90
|
// `<html data-display-mode="browser|standalone|...">` for CSS (see styles/css/base.css tokens).
|
|
90
91
|
React.useEffect(() => syncDisplayModeAttribute(), []);
|
|
92
|
+
// `--app-height` measured in an installed app (iOS standalone reports dvh/100% short; see hooks/media/appHeight).
|
|
93
|
+
React.useEffect(() => syncAppHeight(), []);
|
|
91
94
|
// No SSR-skip on purpose: any nested library component that renders
|
|
92
95
|
// `<Tooltip>` on its first paint expects to find `<TooltipProvider>`
|
|
93
96
|
// already in the tree. Skipping the provider during SSR caused
|
package/src/styles/css/base.css
CHANGED
|
@@ -145,6 +145,15 @@ html {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/* Installed app, first paint: iOS reports 100dvh short by the top inset with viewport-fit=cover, 100lvh is the full
|
|
149
|
+
* screen. `syncAppHeight()` (UiProviders) then replaces this with the measured px value on <html>. */
|
|
150
|
+
@supports (height: 100lvh) {
|
|
151
|
+
@media (display-mode: standalone), (display-mode: fullscreen) {
|
|
152
|
+
:root { --app-height: 100lvh; }
|
|
153
|
+
}
|
|
154
|
+
:root:is([data-display-mode="standalone"], [data-display-mode="fullscreen"]) { --app-height: 100lvh; }
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
/* Installed app (any display mode the display-mode hook treats as a PWA): behave like a native shell.
|
|
149
158
|
* `data-display-mode` is set on <html> by UiProviders. */
|
|
150
159
|
:root:is([data-display-mode="standalone"], [data-display-mode="fullscreen"], [data-display-mode="minimal-ui"], [data-display-mode="window-controls-overlay"]),
|
|
@@ -158,7 +167,8 @@ html {
|
|
|
158
167
|
* attribute and stay scrollable. */
|
|
159
168
|
:root[data-private-shell]:is([data-display-mode="standalone"], [data-display-mode="fullscreen"], [data-display-mode="minimal-ui"], [data-display-mode="window-controls-overlay"]),
|
|
160
169
|
:root[data-private-shell]:is([data-display-mode="standalone"], [data-display-mode="fullscreen"], [data-display-mode="minimal-ui"], [data-display-mode="window-controls-overlay"]) body {
|
|
161
|
-
|
|
170
|
+
/* Not `100%`: in an installed iOS app with viewport-fit=cover it resolves short by the top inset. */
|
|
171
|
+
height: var(--app-height, 100dvh);
|
|
162
172
|
overflow: hidden;
|
|
163
173
|
}
|
|
164
174
|
|