@wippy-fe/theme 0.0.42 → 0.0.44
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/THEMING.md +16 -0
- package/package.json +1 -1
- package/primevue/common.css +28 -0
- package/primevue-plugin.ts +244 -8
- package/tokens.json +1 -1
package/THEMING.md
CHANGED
|
@@ -676,6 +676,22 @@ The CSS files use theme variables via Tailwind classes. Example from `button.css
|
|
|
676
676
|
|
|
677
677
|
The `tailwind.css` master file imports all component CSS files organized by category (form, button, data, overlay, menu, panel, file, message, media, misc).
|
|
678
678
|
|
|
679
|
+
### Overlays render inside the shadow root
|
|
680
|
+
|
|
681
|
+
PrimeVue overlays — `Dialog`, `Drawer`, `Select`, `Popover`, `Menu`, `MultiSelect`, … — teleport their panel through one shared primitive (`primevue/portal`) whose default target is `document.body`. Inside a **web component** that escapes the shadow root, so the panel loses the module's shadow-scoped CSS (and the injected `primeVueCssUrl`) and renders unstyled next to `<body>`.
|
|
682
|
+
|
|
683
|
+
`PrimeVuePlugin` handles this for you: at `install()` it rewrites the shared Portal so the default `appendTo` (`'body'`) teleports each overlay to a per-shadow-root `position: fixed` zero-size anchor. The overlay stays **inside the shadow root** (so the shadow-scoped CSS and the injected `primeVueCssUrl` apply) and — because `appendTo` is not `'self'` — PrimeVue positions it with `absolutePosition` at the trigger, **unclipped** by any scrolling ancestor (e.g. a `Dialog` / `Drawer` body with `overflow: auto`). Outside a shadow root (the host SPA) it falls back to `document.body`. You do nothing per component.
|
|
684
|
+
|
|
685
|
+
The anchor is **measured, not assumed**. `absolutePosition` writes *document* coordinates, so the anchor must sit at the true viewport origin for an absolutely-positioned overlay to land correctly. A `position: fixed` element is only at the viewport origin when no ancestor establishes a containing block — but `transform` / `filter` / `perspective` / `will-change` / `contain` on any ancestor (a `translateZ(0)` GPU hint, a slide/scale animation, a virtualized list) makes *that* ancestor the containing block instead, and a scrolled window shifts it too. So the plugin reads the anchor's own `getBoundingClientRect()` and offsets it by `-scrollX / -scrollY` to pin it at viewport `(0,0)`, re-syncing when an overlay opens (a `MutationObserver`) and on scroll/resize. This is what keeps overlays aligned under a transformed ancestor or a scrolled page. A reactive `append-to` is honored (`resolvedTarget` is a computed). **`Tooltip` is the one exception** — it is a directive that appends to `document.body`, bypassing `primevue/portal`, so it renders in the light DOM and is not shadow-scoped by this plugin.
|
|
686
|
+
|
|
687
|
+
> **Do not set `append-to="self"`.** `'self'` makes PrimeVue render the overlay *inline* and position it relatively, so an overlay whose trigger sits inside a scrolling `Dialog` / `Drawer` body gets clipped — this was the regression in `@wippy-fe/theme` ≤ 0.0.43 (web-host ≤ 1.0.43). With this plugin, leave `appendTo` unset and inherit the shadow-root portal. (PrimeVue 4 has no global `appendTo` config and the `@primeuix/utils` positioning helpers are immutable, but the shared Portal is a singleton rewritten once at install — no per-app `patch-package` needed.)
|
|
688
|
+
>
|
|
689
|
+
> **Residuals** (all uncommon; none affects a static, translation-only layout):
|
|
690
|
+
> - **Scale / rotate / skew ancestor:** the compensation cancels a containing block's *translation* only, so a `scale()`/`rotate()`/`skew()` ancestor mis-positions the overlay. `translate` / `translateZ(0)` (the common case) is handled.
|
|
691
|
+
> - **Clipping:** an ancestor that both establishes the fixed containing block *and* clips (`transform`/`contain`/`will-change` + `overflow != visible`, or `contain: paint/strict/content`, or `overflow: clip`) can still clip an overlay extending beyond its box — intrinsic to staying in the shadow root.
|
|
692
|
+
> - **Animating ancestor:** a transform animating while an overlay is open lags until the next scroll/resize.
|
|
693
|
+
> - **`Toast`:** it self-positions `position: fixed`, so under a transformed ancestor it resolves against that ancestor, not the viewport. Wippy routes toasts to the host, so this isn't hit in practice.
|
|
694
|
+
|
|
679
695
|
### Available PrimeVue component styles
|
|
680
696
|
|
|
681
697
|
**Form**: autocomplete, cascadeselect, checkbox, colorpicker, datepicker, iconfield, iftalabel, inputgroup, inputnumber, inputotp, inputtext, floatlabel, knob, listbox, multiselect, password, radiobutton, rating, select, selectbutton, slider, textarea, togglebutton, toggleswitch, treeselect
|
package/package.json
CHANGED
package/primevue/common.css
CHANGED
|
@@ -50,6 +50,34 @@
|
|
|
50
50
|
@apply w-4 h-4
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/*
|
|
54
|
+
* Screen-reader-only content. PrimeVue marks accessibility text with these classes
|
|
55
|
+
* and expects the theme's base CSS to hide them — e.g. Select's `aria-live` selection
|
|
56
|
+
* announcer ("No selected item"), and dialog focus-trap sentinels. In `theme: 'none'`
|
|
57
|
+
* mode we own the base CSS, so without this the announcer text renders VISIBLY (it
|
|
58
|
+
* showed up under an open Select once overlays moved into the shadow root). Matches
|
|
59
|
+
* PrimeVue's canonical `.p-hidden-accessible` definition.
|
|
60
|
+
*/
|
|
61
|
+
.p-hidden-accessible,
|
|
62
|
+
.p-hidden-focusable {
|
|
63
|
+
border: 0;
|
|
64
|
+
clip: rect(0 0 0 0);
|
|
65
|
+
clip-path: inset(50%);
|
|
66
|
+
height: 1px;
|
|
67
|
+
margin: -1px;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
padding: 0;
|
|
70
|
+
position: absolute;
|
|
71
|
+
width: 1px;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
user-select: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.p-hidden-accessible input,
|
|
77
|
+
.p-hidden-accessible select {
|
|
78
|
+
transform: scale(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
.p-overlay-mask {
|
|
54
82
|
@apply bg-black/50 text-surface-200 transition-colors duration-150 fixed top-0 start-0 w-full h-full
|
|
55
83
|
}
|
package/primevue-plugin.ts
CHANGED
|
@@ -1,8 +1,244 @@
|
|
|
1
|
-
import type { App } from 'vue'
|
|
2
|
-
import PrimeVue from 'primevue/config'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { App, VNode } from 'vue'
|
|
2
|
+
import PrimeVue from 'primevue/config'
|
|
3
|
+
import Portal from 'primevue/portal'
|
|
4
|
+
import { createCommentVNode, h, renderSlot, Teleport } from 'vue'
|
|
5
|
+
|
|
6
|
+
const LAYER_ATTR = 'data-wippy-overlay-layer'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Client (browser) guard, equivalent to `@primeuix/utils`' `isClient` — inlined so
|
|
10
|
+
* this plugin (which ships as raw `.ts` that every consumer typechecks) takes no
|
|
11
|
+
* dependency on `@primeuix/utils`, which is only a transitive dep of `primevue` and
|
|
12
|
+
* is not guaranteed to resolve at the `/dom` subpath in a consumer's build.
|
|
13
|
+
*/
|
|
14
|
+
function isClient(): boolean {
|
|
15
|
+
return typeof window !== 'undefined' && typeof window.document !== 'undefined'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The `ShadowRoot` a node lives in, or null (light DOM). */
|
|
19
|
+
function shadowRootOf(el: Node | null): ShadowRoot | null {
|
|
20
|
+
const root = el && typeof el.getRootNode === 'function' ? el.getRootNode() : null
|
|
21
|
+
return root instanceof ShadowRoot ? root : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pin `layer` (a `position: fixed` element) so its viewport origin is exactly
|
|
26
|
+
* `(-scrollX, -scrollY)` — i.e. document coordinate `(0,0)` maps to the layer's
|
|
27
|
+
* top-left corner.
|
|
28
|
+
*
|
|
29
|
+
* PrimeVue's `absolutePosition` writes DOCUMENT coordinates to an overlay
|
|
30
|
+
* (`trigger.getBoundingClientRect()` + window scroll). The overlay is
|
|
31
|
+
* `position: absolute`, so those coordinates resolve against this layer. Placing
|
|
32
|
+
* the layer at `(-scrollX, -scrollY)` makes an absolute child at document coord D
|
|
33
|
+
* render at viewport `D - scroll` — the correct on-screen spot.
|
|
34
|
+
*
|
|
35
|
+
* We MEASURE instead of assuming "fixed == viewport origin": a fixed element's
|
|
36
|
+
* containing block is the nearest ancestor with `transform` / `filter` /
|
|
37
|
+
* `perspective` / `will-change` / `contain` (extremely common — `translateZ(0)`
|
|
38
|
+
* GPU hints, slide animations, virtualized lists), NOT necessarily the viewport.
|
|
39
|
+
* Reading the rect at `top:0;left:0` yields that containing block's viewport
|
|
40
|
+
* origin; we then offset by it plus the scroll. This holds for any
|
|
41
|
+
* TRANSLATION-only containing block and any window scroll.
|
|
42
|
+
*
|
|
43
|
+
* Residuals (documented):
|
|
44
|
+
* - SCALE / ROTATE / SKEW containing block: compensating via CSS `top`/`left`
|
|
45
|
+
* only cancels the containing block's translation, not a non-identity linear
|
|
46
|
+
* transform, so a `scale()`/`rotate()`/`skew()` ancestor mispositions the
|
|
47
|
+
* overlay (by `(s-1)·distance` under `scale(s)`). Rare for a shadow-host
|
|
48
|
+
* ancestor; a full fix would need the ancestor's transform matrix. Translation
|
|
49
|
+
* (`translate`, `translateZ(0)`) is the common case and is handled.
|
|
50
|
+
* - CLIPPING: an ancestor that both establishes the fixed containing block AND
|
|
51
|
+
* clips (`transform`/`contain`/`will-change` + `overflow != visible`, or
|
|
52
|
+
* `contain: paint/strict/content`, or `overflow: clip`) can still clip an
|
|
53
|
+
* overlay that extends beyond its box — intrinsic to staying in the shadow root.
|
|
54
|
+
* - ANIMATING ancestor: if the containing block MOVES between resyncs (a
|
|
55
|
+
* transform animating while an overlay is open) the overlay lags until the
|
|
56
|
+
* next scroll/resize.
|
|
57
|
+
* - `position: fixed` self-positioning overlays (Toast) resolve against a
|
|
58
|
+
* transformed containing block, not the viewport, so they are offset under such
|
|
59
|
+
* an ancestor (their own fixed CSS, not `absolutePosition`, is unaffected by the
|
|
60
|
+
* layer). Wippy bridges toasts to the host, so this is not hit in practice.
|
|
61
|
+
*/
|
|
62
|
+
function syncLayer(layer: HTMLElement): void {
|
|
63
|
+
layer.style.top = '0px'
|
|
64
|
+
layer.style.left = '0px'
|
|
65
|
+
const rect = layer.getBoundingClientRect()
|
|
66
|
+
layer.style.top = `${-window.scrollY - rect.top}px`
|
|
67
|
+
layer.style.left = `${-window.scrollX - rect.left}px`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A SINGLE viewport-driven resync loop shared by every shadow-root layer — not a
|
|
71
|
+
// listener pair per layer. When a `view.component` unmounts, its layer detaches with
|
|
72
|
+
// the shadow root; this loop prunes detached layers on the next scroll/resize, so a
|
|
73
|
+
// mount/unmount cycle leaks no window listeners, observers, or detached subtree.
|
|
74
|
+
const activeLayers = new Set<HTMLElement>()
|
|
75
|
+
const layerObservers = new WeakMap<HTMLElement, MutationObserver>()
|
|
76
|
+
let viewportListenersAttached = false
|
|
77
|
+
let viewportSyncScheduled = false
|
|
78
|
+
|
|
79
|
+
function syncActiveLayers(): void {
|
|
80
|
+
viewportSyncScheduled = false
|
|
81
|
+
for (const layer of activeLayers) {
|
|
82
|
+
if (!layer.isConnected) {
|
|
83
|
+
activeLayers.delete(layer)
|
|
84
|
+
layerObservers.get(layer)?.disconnect()
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (layer.childElementCount > 0)
|
|
88
|
+
syncLayer(layer)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onViewportChange(): void {
|
|
93
|
+
if (viewportSyncScheduled)
|
|
94
|
+
return
|
|
95
|
+
viewportSyncScheduled = true
|
|
96
|
+
requestAnimationFrame(syncActiveLayers)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get (or create) the per-shadow-root overlay layer, pinned to viewport origin and
|
|
101
|
+
* kept in sync as overlays open (a per-layer `MutationObserver`) and as the page
|
|
102
|
+
* scrolls/resizes (the one shared listener pair over {@link activeLayers}).
|
|
103
|
+
*/
|
|
104
|
+
function ensureLayer(root: ShadowRoot): HTMLElement {
|
|
105
|
+
const existing = root.querySelector<HTMLElement>(`[${LAYER_ATTR}]`)
|
|
106
|
+
if (existing)
|
|
107
|
+
return existing
|
|
108
|
+
|
|
109
|
+
const layer = document.createElement('div')
|
|
110
|
+
layer.setAttribute(LAYER_ATTR, '')
|
|
111
|
+
layer.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;'
|
|
112
|
+
root.appendChild(layer)
|
|
113
|
+
syncLayer(layer)
|
|
114
|
+
|
|
115
|
+
// Re-pin whenever an overlay is inserted — it may open after the page scrolled or
|
|
116
|
+
// the layout shifted (the layer isn't synced while empty). syncLayer only mutates
|
|
117
|
+
// the layer's own style, so it never re-triggers this childList observer. The
|
|
118
|
+
// observer is referenced only via the WeakMap, so it is disconnected when the layer
|
|
119
|
+
// detaches (in syncActiveLayers) and otherwise GC'd with the layer.
|
|
120
|
+
const observer = new MutationObserver(() => {
|
|
121
|
+
if (layer.isConnected && layer.childElementCount > 0)
|
|
122
|
+
syncLayer(layer)
|
|
123
|
+
})
|
|
124
|
+
observer.observe(layer, { childList: true })
|
|
125
|
+
layerObservers.set(layer, observer)
|
|
126
|
+
|
|
127
|
+
activeLayers.add(layer)
|
|
128
|
+
if (!viewportListenersAttached) {
|
|
129
|
+
viewportListenersAttached = true
|
|
130
|
+
// Capture phase so a scrolling ancestor (not just the window) also triggers a
|
|
131
|
+
// resync; passive since we never preventDefault. One pair total, for all layers.
|
|
132
|
+
window.addEventListener('scroll', onViewportChange, { capture: true, passive: true })
|
|
133
|
+
window.addEventListener('resize', onViewportChange, { passive: true })
|
|
134
|
+
}
|
|
135
|
+
return layer
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* PrimeVue's shared `primevue/portal` is an Options-API component object (a singleton
|
|
140
|
+
* shared by every overlay). We rewrite the render-relevant options on it in place.
|
|
141
|
+
*/
|
|
142
|
+
interface PortalOptions {
|
|
143
|
+
computed?: Record<string, (this: PortalInstance) => unknown>
|
|
144
|
+
data?: (this: PortalInstance) => Record<string, unknown>
|
|
145
|
+
mounted?: (this: PortalInstance) => void
|
|
146
|
+
render?: (this: PortalInstance) => VNode
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface PortalInstance {
|
|
150
|
+
/** The Portal's `$el` — a comment `Node` (placeholder vnode) before teleport. */
|
|
151
|
+
$el: Node | null
|
|
152
|
+
$slots: Record<string, unknown>
|
|
153
|
+
disabled?: boolean
|
|
154
|
+
/** `'self'` (inline), `'body'`/null/undefined (teleport), or an explicit target. */
|
|
155
|
+
appendTo?: unknown
|
|
156
|
+
inline: boolean
|
|
157
|
+
mounted: boolean
|
|
158
|
+
/** The shadow root this overlay lives in (captured once on mount), or null. */
|
|
159
|
+
hostRoot: ShadowRoot | null
|
|
160
|
+
resolvedTarget: Element | string | null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let patched = false
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Make PrimeVue overlays render inside the shadow root — unclipped and correctly
|
|
167
|
+
* positioned — by default, without any per-component `appendTo`.
|
|
168
|
+
*
|
|
169
|
+
* PrimeVue 4 has no global `appendTo` config, and the `@primeuix/utils` positioning
|
|
170
|
+
* helpers are immutable ESM bindings. But every overlay teleports through the ONE
|
|
171
|
+
* shared `primevue/portal`, whose options object is a mutable singleton. We rewrite it
|
|
172
|
+
* so the DEFAULT `appendTo` (`'body'`, or null/undefined) teleports to a per-shadow-root
|
|
173
|
+
* layer ({@link ensureLayer}) that is measured-and-pinned to true viewport origin
|
|
174
|
+
* ({@link syncLayer}) instead of `document.body`.
|
|
175
|
+
*
|
|
176
|
+
* Because `appendTo` is then not `'self'`, PrimeVue positions with `absolutePosition`
|
|
177
|
+
* (document-origin coords), which the pinned layer matches — so overlays are NOT
|
|
178
|
+
* clipped by a scrolling Dialog/Drawer body and land at the trigger, even under a
|
|
179
|
+
* transformed/filtered ancestor or a scrolled window. `'self'` keeps its inline
|
|
180
|
+
* semantics (explicit opt-in); outside a shadow root everything falls back to
|
|
181
|
+
* `document.body`. `resolvedTarget` is a computed, so a reactive `appendTo` is honored.
|
|
182
|
+
*
|
|
183
|
+
* Runs once at plugin install (before the first overlay renders); the guard makes
|
|
184
|
+
* repeat installs safe.
|
|
185
|
+
*/
|
|
186
|
+
export function patchOverlayPortal(): void {
|
|
187
|
+
if (patched)
|
|
188
|
+
return
|
|
189
|
+
patched = true
|
|
190
|
+
|
|
191
|
+
const portal = Portal as unknown as PortalOptions
|
|
192
|
+
|
|
193
|
+
portal.computed = portal.computed || {}
|
|
194
|
+
portal.computed.inline = function (this: PortalInstance) {
|
|
195
|
+
return Boolean(this.disabled) || this.appendTo === 'self'
|
|
196
|
+
}
|
|
197
|
+
// Reactive to `appendTo`: the default ('body'/null) resolves to the shadow-root
|
|
198
|
+
// layer (or document.body in light DOM); an explicit target passes through.
|
|
199
|
+
portal.computed.resolvedTarget = function (this: PortalInstance) {
|
|
200
|
+
if (this.appendTo == null || this.appendTo === 'body')
|
|
201
|
+
return this.hostRoot ? ensureLayer(this.hostRoot) : document.body
|
|
202
|
+
return this.appendTo as Element | string
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
portal.data = function (this: PortalInstance) {
|
|
206
|
+
return { mounted: false, hostRoot: null }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
portal.mounted = function (this: PortalInstance) {
|
|
210
|
+
// Match upstream Portal: teleport is client-only. Guards DOM access below
|
|
211
|
+
// (ensureLayer → createElement) if this ever runs without a document.
|
|
212
|
+
if (!isClient())
|
|
213
|
+
return
|
|
214
|
+
// `$el` here is the placeholder comment node — capture its shadow root once.
|
|
215
|
+
this.hostRoot = shadowRootOf(this.$el)
|
|
216
|
+
this.mounted = true
|
|
217
|
+
// Ensure the layer (and its resync observers) exist before the first render.
|
|
218
|
+
// The per-open re-pin is driven by the layer's MutationObserver, not here —
|
|
219
|
+
// this hook fires when the overlay component mounts, not when it opens.
|
|
220
|
+
if (this.hostRoot)
|
|
221
|
+
ensureLayer(this.hostRoot)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
portal.render = function (this: PortalInstance) {
|
|
225
|
+
// Keys mirror upstream's `key: 0`/`key: 1` so Vue replaces (never patches)
|
|
226
|
+
// across the inline↔teleport branches if `inline` ever toggles reactively.
|
|
227
|
+
if (this.inline)
|
|
228
|
+
return renderSlot(this.$slots as never, 'default', { key: 0 }) as unknown as VNode
|
|
229
|
+
if (!this.mounted)
|
|
230
|
+
return createCommentVNode('', true)
|
|
231
|
+
return h(Teleport, { key: 1, to: this.resolvedTarget || document.body } as never, [
|
|
232
|
+
renderSlot(this.$slots as never, 'default'),
|
|
233
|
+
])
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const PrimeVuePlugin = {
|
|
238
|
+
install: (app: App) => {
|
|
239
|
+
patchOverlayPortal()
|
|
240
|
+
app.use(PrimeVue, { theme: 'none' })
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default PrimeVuePlugin
|
package/tokens.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"generatedFrom": "theme-config.css",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"grammar": "every color family = --p-<fam> + --p-<fam>-{50..950} + --p-<fam>-{color,contrast-color,hover-color,active-color}; --p-surface-<shade>; text/content/highlight semantic aliases; --p-font-<role>-<prop>",
|
|
5
5
|
"families": [
|
|
6
6
|
"primary",
|