@uniweb/kit 0.9.40 → 0.9.42
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": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.42",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"fuse.js": "^7.0.0",
|
|
41
41
|
"shiki": "^3.0.0",
|
|
42
42
|
"tailwind-merge": "^3.6.0",
|
|
43
|
-
"@uniweb/core": "0.7.
|
|
43
|
+
"@uniweb/core": "0.7.32",
|
|
44
44
|
"@uniweb/scene": "0.1.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay — render above the page, from anywhere in the tree.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists (it is not "a portal wrapper")
|
|
5
|
+
*
|
|
6
|
+
* The runtime gives every layout area its own `view-transition-name` so the
|
|
7
|
+
* browser can animate header, sidebars and body independently instead of
|
|
8
|
+
* crossfading the whole page. That is on by default.
|
|
9
|
+
*
|
|
10
|
+
* A `view-transition-name` makes the element **a stacking context** and **a
|
|
11
|
+
* containing block for fixed-position descendants**. Both consequences bite
|
|
12
|
+
* the most ordinary thing a foundation builds: a modal opened from the header.
|
|
13
|
+
*
|
|
14
|
+
* div[view-transition-name: uw-header] ← area wrapper, z-index: auto
|
|
15
|
+
* └ div.fixed.inset-0.z-[100] ← your modal
|
|
16
|
+
* div[view-transition-name: uw-body] ← sibling area, z-index: auto
|
|
17
|
+
*
|
|
18
|
+
* The modal's `z-index: 100` is scoped *inside* `uw-header`. Both wrappers sit
|
|
19
|
+
* at `auto` in the root stacking context, so they paint in DOM order — and the
|
|
20
|
+
* body comes second. No z-index a foundation can write will lift the modal
|
|
21
|
+
* above the page content, because the two are not competing in the same
|
|
22
|
+
* stacking context. Raising the number looks like it should work and never
|
|
23
|
+
* does, which is what makes this worth a component rather than a doc note.
|
|
24
|
+
*
|
|
25
|
+
* Rendering into `document.body` leaves every area wrapper behind, so the
|
|
26
|
+
* overlay competes in the root stacking context where its z-index means what
|
|
27
|
+
* the author expects.
|
|
28
|
+
*
|
|
29
|
+
* ## Usage
|
|
30
|
+
*
|
|
31
|
+
* {isOpen && (
|
|
32
|
+
* <Overlay onClose={close}>
|
|
33
|
+
* <div role="dialog" aria-modal="true">…</div>
|
|
34
|
+
* </Overlay>
|
|
35
|
+
* )}
|
|
36
|
+
*
|
|
37
|
+
* Owns only what every overlay needs and nothing about how it looks: the
|
|
38
|
+
* portal, the scrim, Escape, a click outside, and the page-scroll lock. The
|
|
39
|
+
* dialog itself — its shape, its focus handling, its content — is the
|
|
40
|
+
* foundation's design, the same way kit ships no layout.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { useEffect } from 'react'
|
|
44
|
+
import { createPortal } from 'react-dom'
|
|
45
|
+
import { cn } from '../../utils/index.js'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} props
|
|
49
|
+
* @param {React.ReactNode} props.children - The overlay content (your dialog).
|
|
50
|
+
* @param {Function} [props.onClose] - Called on Escape and on a scrim click.
|
|
51
|
+
* Omit for a non-dismissible overlay.
|
|
52
|
+
* @param {boolean} [props.lockScroll=true] - Prevent the page behind from
|
|
53
|
+
* scrolling while open.
|
|
54
|
+
* @param {boolean} [props.closeOnEscape=true]
|
|
55
|
+
* @param {boolean} [props.closeOnScrimClick=true]
|
|
56
|
+
* @param {string} [props.className] - Classes for the scrim/positioning layer.
|
|
57
|
+
* Defaults place the content centred near the top, the usual command-palette
|
|
58
|
+
* position; pass your own to change it.
|
|
59
|
+
* @param {number|string} [props.zIndex=100]
|
|
60
|
+
*/
|
|
61
|
+
export function Overlay({
|
|
62
|
+
children,
|
|
63
|
+
onClose,
|
|
64
|
+
lockScroll = true,
|
|
65
|
+
closeOnEscape = true,
|
|
66
|
+
closeOnScrimClick = true,
|
|
67
|
+
className,
|
|
68
|
+
zIndex = 100,
|
|
69
|
+
...rest
|
|
70
|
+
}) {
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!closeOnEscape || !onClose) return
|
|
73
|
+
const onKeyDown = (event) => {
|
|
74
|
+
if (event.key === 'Escape') {
|
|
75
|
+
event.preventDefault()
|
|
76
|
+
onClose(event)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Capture phase: a dialog's own input may stop propagation on keydown,
|
|
80
|
+
// and Escape still has to close the thing it is typed into.
|
|
81
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
82
|
+
return () => document.removeEventListener('keydown', onKeyDown, true)
|
|
83
|
+
}, [closeOnEscape, onClose])
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!lockScroll) return
|
|
87
|
+
// Restore the previous value rather than clearing: two stacked overlays
|
|
88
|
+
// would otherwise have the inner one unlock the page on close.
|
|
89
|
+
const previous = document.body.style.overflow
|
|
90
|
+
document.body.style.overflow = 'hidden'
|
|
91
|
+
return () => {
|
|
92
|
+
document.body.style.overflow = previous
|
|
93
|
+
}
|
|
94
|
+
}, [lockScroll])
|
|
95
|
+
|
|
96
|
+
// No DOM: prerender and any Node render. An overlay is by definition
|
|
97
|
+
// interactive, so there is nothing meaningful to emit into static HTML —
|
|
98
|
+
// and emitting it would put a scrim over the prerendered page.
|
|
99
|
+
if (typeof document === 'undefined') return null
|
|
100
|
+
|
|
101
|
+
return createPortal(
|
|
102
|
+
<div
|
|
103
|
+
className={cn('fixed inset-0 flex items-start justify-center', className)}
|
|
104
|
+
style={{ zIndex }}
|
|
105
|
+
onClick={closeOnScrimClick && onClose ? (e) => {
|
|
106
|
+
// Only a click on the scrim itself, never one that bubbled out of the
|
|
107
|
+
// dialog — otherwise selecting text and releasing outside closes it.
|
|
108
|
+
if (e.target === e.currentTarget) onClose(e)
|
|
109
|
+
} : undefined}
|
|
110
|
+
{...rest}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</div>,
|
|
114
|
+
document.body
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default Overlay
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Overlay, default } from './Overlay.jsx'
|
package/src/hooks/useShortcut.js
CHANGED
|
@@ -191,10 +191,22 @@ export function matchesShortcut(event, descriptor) {
|
|
|
191
191
|
* `userAgentData.platform` is the modern signal; `navigator.platform` is
|
|
192
192
|
* deprecated but still the most widely accurate fallback.
|
|
193
193
|
*
|
|
194
|
-
*
|
|
194
|
+
* **A `navigator` check alone is not enough, and the failure is nasty.**
|
|
195
|
+
* Node 21+ defines `globalThis.navigator`, and on macOS its `platform` reads
|
|
196
|
+
* `'MacIntel'` — so during prerender this returned true and baked ⌘ into
|
|
197
|
+
* static HTML based on the machine that ran the BUILD. Every visitor then got
|
|
198
|
+
* the build box's platform, and the same source produced different output on a
|
|
199
|
+
* Mac laptop and a Linux CI runner.
|
|
200
|
+
*
|
|
201
|
+
* `document` is the discriminator: Node defines a navigator but never a DOM.
|
|
202
|
+
* Prerender therefore emits the non-Apple spelling, and the client corrects it
|
|
203
|
+
* on mount — which is safe here because the runtime always createRoots and
|
|
204
|
+
* never hydrates, so there is no mismatch to reconcile.
|
|
205
|
+
*
|
|
206
|
+
* @returns {boolean} false anywhere that is not a real browser.
|
|
195
207
|
*/
|
|
196
208
|
export function isApplePlatform() {
|
|
197
|
-
if (typeof navigator === 'undefined') return false
|
|
209
|
+
if (typeof document === 'undefined' || typeof navigator === 'undefined') return false
|
|
198
210
|
const modern = navigator.userAgentData?.platform
|
|
199
211
|
if (typeof modern === 'string' && modern) return /mac|ios|iphone|ipad/i.test(modern)
|
|
200
212
|
return /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '')
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
// Navigation
|
|
16
16
|
export { Link } from './components/Link/index.js'
|
|
17
17
|
export { Image } from './components/Image/index.js'
|
|
18
|
+
// Renders above the page from anywhere in the tree — see Overlay.jsx for why
|
|
19
|
+
// a plain fixed+z-index element cannot escape a layout area.
|
|
20
|
+
export { Overlay } from './components/Overlay/index.js'
|
|
18
21
|
export { SafeHtml } from './components/SafeHtml/index.js'
|
|
19
22
|
export { Icon } from './components/Icon/index.js'
|
|
20
23
|
|