@wippy-fe/theme 0.0.64 → 0.0.65

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 CHANGED
@@ -680,10 +680,14 @@ The `tailwind.css` master file imports all component CSS files organized by cate
680
680
 
681
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
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.
683
+ `PrimeVuePlugin` handles this for you: at `install()` it rewrites the shared Portal so the default `appendTo` (`'body'`) teleports each overlay to its own `position: fixed` zero-size layer in the component's shadow root. 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
684
 
685
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
686
 
687
+ **Overlays follow the component's forced theme.** The Host puts `w-theme-dark` or `w-theme-light` on the container a web component renders into, but each overlay's layer is a sibling of that container. Anything keyed on that class would miss the overlay: the Tailwind `dark:` utilities, and the in-shadow `.w-theme-*` rule that sets the `--p-*` tokens and `color-scheme`. A calendar or a Select opened in a dark-forced app would then paint in the operating system's scheme, which is white on a light system. So each layer copies the theme class of the nearest theme context above the overlay's trigger. A theme context is an element that carries a forced class, or one marked `data-wippy-theme-root`. The search runs through the ancestors inside the shadow root and ends at the host element, so a component that mirrors the class onto its host element and nowhere inside still works. The host element counts only when the shadow root has no theme root of its own: a marked container in Auto mode is the nearest context, and the layer then carries no class even if the host is forced. The layer keeps the copy in sync while the theme changes, including Auto following the operating system, and a nearer element that becomes themed later takes over. The page never counts. With no theme context, or an Auto-mode root, the layer carries no class.
688
+
689
+ While the layer carries a theme class it also carries `data-wippy-theme-root`. The `.w-theme-*` rule declares every default `--p-*` token again on any element with the class, which would replace the facade's `cssVariables` overrides that the layer otherwise inherits from the host element. The variable bridge in `@wippy-fe/webcomponent-core` selects `:where([data-wippy-theme-root]).w-theme-*` and restores the inherited values, so the layer is treated like the container. A component does not need to mirror the class onto its overlays itself.
690
+
687
691
  > **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
692
  >
689
693
  > **Residuals** (all uncommon; none affects a static, translation-only layout):
@@ -919,6 +923,7 @@ The resizable panel divider between main content and the right panel.
919
923
  | `.chat-input__attach-button` | Attachment button |
920
924
  | `.chat-input__send-button` | Send button |
921
925
  | `.chat-input__stop-button` | Stop generation button |
926
+ | `.chat-input__stop-button--stopping` | Stop button after a click, while the Host waits for the turn to stop (also `aria-busy="true"`); the default style dims the icon |
922
927
  | `.chat-input__upload-list` | Upload queue list |
923
928
  | `.chat-input__prompts` | Suggested prompts area |
924
929
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "implementationRelease": "1.0.64",
3
+ "implementationRelease": "1.0.65",
4
4
  "upstream": {
5
5
  "package": "@primeuix/themes",
6
6
  "version": "2.0.3",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "packageVersion": "0.0.64",
4
- "implementationRelease": "1.0.64",
3
+ "packageVersion": "0.0.65",
4
+ "implementationRelease": "1.0.65",
5
5
  "compatibility": {
6
- "themePackageVersion": "0.0.64",
6
+ "themePackageVersion": "0.0.65",
7
7
  "manifestSchemaVersion": "1.0.0",
8
- "implementationRelease": "1.0.64",
8
+ "implementationRelease": "1.0.65",
9
9
  "tailwindVersion": "3.4.19",
10
10
  "tailwindcssPrimeuiVersion": "0.6.1",
11
11
  "primevueVersion": "4.5.5",
@@ -14,7 +14,7 @@
14
14
  "referencePrimeuixThemesVersion": "2.0.3",
15
15
  "referencePrimeuixThemesIntegrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==",
16
16
  "sourceHashes": {
17
- "themeContract": "a47fc609d0054e11f71cd21f64160c5be8b1e6ac0641329614da8c81fea87bed",
17
+ "themeContract": "6f45be1a33f15896b27923d9032fc24d4d0d11fd02dc677ea3c9571535253c42",
18
18
  "runtimeAcceptanceRequirements": "6bac23fb52a87b18b1661b5ac73747a972e07f40af6961571b5f4dd831d4cb85",
19
19
  "themeConfig": "9b484e9da76871d11edda5a555b0d0443a262acd80fb42c74c4fd78e2b23b5ae",
20
20
  "tailwindConfig": "f1e862105254f082a78823ea685e3c6dc3ff5822516b7434a1e1141c976adc1d",
@@ -35,7 +35,7 @@
35
35
  "acceptance": {
36
36
  "status": "unverified",
37
37
  "evidenceSchemaVersion": "1.0.0",
38
- "inputHash": "9fdbfaa932b0d0774028e43152812dbbb6a4150651ddb6f85a2e7dc03c56200d",
38
+ "inputHash": "dac4b887da9913a1872ef27a446e6032ec734dd7694410f7decaef1a7c21ab09",
39
39
  "requirements": "contracts/runtime-acceptance-requirements.json",
40
40
  "gate": "node scripts/build-theme-contract.mjs --check --acceptance --evidence <path> --evidence-sha256 <hash>"
41
41
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wippy-fe/theme",
3
- "version": "0.0.64",
3
+ "version": "0.0.65",
4
4
  "description": "Theme variables, shared Tailwind config, and PrimeVue styling for Wippy web components.",
5
5
  "type": "module",
6
6
  "main": "theme-config.css",
@@ -37,7 +37,7 @@ function supportsTopLayer(): boolean {
37
37
  return (
38
38
  isClient()
39
39
  && typeof HTMLElement !== 'undefined'
40
- && Object.hasOwn(HTMLElement.prototype, 'popover')
40
+ && 'popover' in HTMLElement.prototype
41
41
  )
42
42
  }
43
43
 
@@ -229,6 +229,85 @@ const layerObservers = new WeakMap<HTMLElement, MutationObserver>()
229
229
  let viewportListenersAttached = false
230
230
  let viewportSyncScheduled = false
231
231
 
232
+ /** The forced-theme classes the Host and `base-element` put on a themed element. */
233
+ const THEME_MODE_CLASSES = ['w-theme-dark', 'w-theme-light']
234
+
235
+ /**
236
+ * Marks a themed element as a theme root. `@wippy-fe/webcomponent-core`'s facade-variable
237
+ * bridge (`:where([data-wippy-theme-root]).w-theme-*`) keys on it, see {@link followTheme}.
238
+ */
239
+ const THEME_ROOT_ATTR = 'data-wippy-theme-root'
240
+
241
+ /** Per-layer observer that keeps the layer's theme class equal to its trigger context's. */
242
+ const themeObservers = new WeakMap<HTMLElement, MutationObserver>()
243
+
244
+ /**
245
+ * The elements above `from`, nearest first, up to and NOT past its shadow root. A shadow
246
+ * root is not an element, so the walk stops there instead of reaching the host element or
247
+ * the page: the caller decides whether the host element counts.
248
+ */
249
+ function ancestorsInShadow(from: Node | null): Element[] {
250
+ const chain: Element[] = []
251
+ let node: Node | null = from ? from.parentNode : null
252
+ while (node && node.nodeType === 1) {
253
+ chain.push(node as Element)
254
+ node = node.parentNode
255
+ }
256
+ return chain
257
+ }
258
+
259
+ /**
260
+ * Gives `layer` the forced theme of the overlay's trigger context, and keeps it in sync.
261
+ *
262
+ * The Host puts `w-theme-dark` or `w-theme-light` on the container a web component renders
263
+ * into, but the layer is a sibling of that container. Rules keyed on the class (the Tailwind
264
+ * `dark:` utilities, the in-shadow `.w-theme-*` token block and its `color-scheme`) therefore
265
+ * miss the overlay, which paints in the OS scheme. The layer copies the class of the nearest
266
+ * theme context above the trigger, and a theme context is an element that carries a forced
267
+ * class or is marked {@link THEME_ROOT_ATTR}. The search runs through the ancestors inside the
268
+ * shadow root and ends at the shadow host element, which counts only when the shadow root has
269
+ * no theme root of its own, so a component that mirrors the class onto its host element and
270
+ * nowhere inside still works. A theme root with no forced class (Auto mode) ends the search
271
+ * and leaves the layer unthemed, because it is the nearest context. The page never counts.
272
+ *
273
+ * While themed the layer also carries {@link THEME_ROOT_ATTR}, so the variable bridge in
274
+ * `@wippy-fe/webcomponent-core` (`:where([data-wippy-theme-root]).w-theme-*`) restores the
275
+ * facade's `--p-*` overrides that the `.w-theme-*` block would replace. With no theme
276
+ * context the layer carries nothing. One observer watches the `class` and marker attributes of
277
+ * every element in the chain, and {@link unfollowTheme} disconnects it.
278
+ */
279
+ function followTheme(layer: HTMLElement, trigger: Node | null, root: ShadowRoot): void {
280
+ const chain = [...ancestorsInShadow(trigger), root.host]
281
+ const sync = (): void => {
282
+ let themed: Element | undefined
283
+ for (const el of chain) {
284
+ if (THEME_MODE_CLASSES.some(name => el.classList.contains(name))) {
285
+ themed = el
286
+ break
287
+ }
288
+ if (el.hasAttribute(THEME_ROOT_ATTR))
289
+ break
290
+ }
291
+ for (const name of THEME_MODE_CLASSES)
292
+ layer.classList.toggle(name, Boolean(themed && themed.classList.contains(name)))
293
+ if (themed)
294
+ layer.setAttribute(THEME_ROOT_ATTR, '')
295
+ else
296
+ layer.removeAttribute(THEME_ROOT_ATTR)
297
+ }
298
+ sync()
299
+ const observer = new MutationObserver(sync)
300
+ for (const el of chain)
301
+ observer.observe(el, { attributes: true, attributeFilter: ['class', THEME_ROOT_ATTR] })
302
+ themeObservers.set(layer, observer)
303
+ }
304
+
305
+ /** Stop mirroring the theme onto `layer` (it is being removed, or already detached). */
306
+ function unfollowTheme(layer: HTMLElement): void {
307
+ themeObservers.get(layer)?.disconnect()
308
+ themeObservers.delete(layer)
309
+ }
310
+
232
311
  function syncActiveLayers(): void {
233
312
  viewportSyncScheduled = false
234
313
  for (const layer of activeLayers) {
@@ -241,6 +320,7 @@ function syncActiveLayers(): void {
241
320
  }
242
321
  activeLayers.delete(layer)
243
322
  layerObservers.get(layer)?.disconnect()
323
+ unfollowTheme(layer)
244
324
  continue
245
325
  }
246
326
  if (layer.childElementCount > 0)
@@ -255,11 +335,6 @@ function onViewportChange(): void {
255
335
  requestAnimationFrame(syncActiveLayers)
256
336
  }
257
337
 
258
- function syncLayerTheme(layer: HTMLElement, host: Element): void {
259
- layer.classList.toggle('w-theme-light', host.classList.contains('w-theme-light'))
260
- layer.classList.toggle('w-theme-dark', host.classList.contains('w-theme-dark'))
261
- }
262
-
263
338
  /**
264
339
  * Create a fresh per-OVERLAY layer (one per Portal instance, NOT shared per shadow root),
265
340
  * pinned to viewport origin and kept in sync as its overlay opens (a per-layer
@@ -268,13 +343,9 @@ function syncLayerTheme(layer: HTMLElement, host: Element): void {
268
343
  * for components that open several overlays at once; {@link teardownOverlayLayer} removes
269
344
  * it on unmount or a switch to inline.
270
345
  */
271
- function createLayer(root: ShadowRoot, themeSource: Element): HTMLElement {
346
+ function createLayer(root: ShadowRoot): HTMLElement {
272
347
  const layer = document.createElement('div')
273
348
  layer.setAttribute(LAYER_ATTR, '')
274
- // Forced-mode selectors need an in-shadow ancestor. The theme-root marker
275
- // keeps their token defaults from masking inherited Facade custom values.
276
- layer.setAttribute('data-wippy-theme-root', '')
277
- syncLayerTheme(layer, themeSource)
278
349
  const topLayer = supportsTopLayer()
279
350
  // Inline reset of the UA `[popover]` styling — scoped to this one private element,
280
351
  // NEVER a global rule that could leak across modules. A shown popover gets the UA
@@ -327,7 +398,6 @@ function createLayer(root: ShadowRoot, themeSource: Element): HTMLElement {
327
398
  // observer is referenced only via the WeakMap, so it is disconnected when the layer
328
399
  // detaches (in syncActiveLayers) and otherwise GC'd with the layer.
329
400
  const observer = new MutationObserver((records) => {
330
- syncLayerTheme(layer, themeSource)
331
401
  // S2: this layer serves exactly ONE overlay (one Portal instance), so its
332
402
  // `__wippyAnchorName` — the trigger anchor-name stamped by `setupOverlayLayer` — is unambiguous.
333
403
  // Anchor the overlay when it teleports in; release its style observer when it leaves.
@@ -348,7 +418,6 @@ function createLayer(root: ShadowRoot, themeSource: Element): HTMLElement {
348
418
  syncLayer(layer)
349
419
  })
350
420
  observer.observe(layer, { childList: true })
351
- observer.observe(themeSource, { attributes: true, attributeFilter: ['class'] })
352
421
  layerObservers.set(layer, observer)
353
422
 
354
423
  activeLayers.add(layer)
@@ -402,8 +471,9 @@ function setupOverlayLayer(inst: PortalInstance): void {
402
471
  return
403
472
  // Eager: the layer is promoted to the top layer BEFORE the overlay opens, so PrimeVue
404
473
  // never measures a not-yet-promoted box.
405
- const themeSource = inst.$el?.parentElement?.closest('[data-wippy-theme-root], .w-theme-light, .w-theme-dark') ?? inst.hostRoot.host
406
- inst.layer = createLayer(inst.hostRoot, themeSource)
474
+ inst.layer = createLayer(inst.hostRoot)
475
+ // The layer is a sibling of the themed container, so it follows its trigger context's theme class.
476
+ followTheme(inst.layer, inst.$el, inst.hostRoot)
407
477
  // S2: stamp a unique `anchor-name` on the trigger (the placeholder's parent is the
408
478
  // component root the self-anchored families align to) and hand it to this layer.
409
479
  if (supportsAnchor()) {
@@ -431,6 +501,7 @@ function teardownOverlayLayer(inst: PortalInstance): void {
431
501
  }
432
502
  activeLayers.delete(inst.layer)
433
503
  layerObservers.get(inst.layer)?.disconnect()
504
+ unfollowTheme(inst.layer)
434
505
  inst.layer.remove()
435
506
  inst.layer = null
436
507
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "packageVersion": "0.0.64",
3
+ "packageVersion": "0.0.65",
4
4
  "compatibility": {
5
- "themePackageVersion": "0.0.64",
5
+ "themePackageVersion": "0.0.65",
6
6
  "manifestSchemaVersion": "1.0.0",
7
- "implementationRelease": "1.0.64",
7
+ "implementationRelease": "1.0.65",
8
8
  "tailwindVersion": "3.4.19",
9
9
  "tailwindcssPrimeuiVersion": "0.6.1",
10
10
  "primevueVersion": "4.5.5",
@@ -13,7 +13,7 @@
13
13
  "referencePrimeuixThemesVersion": "2.0.3",
14
14
  "referencePrimeuixThemesIntegrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==",
15
15
  "sourceHashes": {
16
- "themeContract": "a47fc609d0054e11f71cd21f64160c5be8b1e6ac0641329614da8c81fea87bed",
16
+ "themeContract": "6f45be1a33f15896b27923d9032fc24d4d0d11fd02dc677ea3c9571535253c42",
17
17
  "runtimeAcceptanceRequirements": "6bac23fb52a87b18b1661b5ac73747a972e07f40af6961571b5f4dd831d4cb85",
18
18
  "themeConfig": "9b484e9da76871d11edda5a555b0d0443a262acd80fb42c74c4fd78e2b23b5ae",
19
19
  "tailwindConfig": "f1e862105254f082a78823ea685e3c6dc3ff5822516b7434a1e1141c976adc1d",
@@ -34,7 +34,7 @@
34
34
  "acceptance": {
35
35
  "status": "unverified",
36
36
  "evidenceSchemaVersion": "1.0.0",
37
- "inputHash": "9fdbfaa932b0d0774028e43152812dbbb6a4150651ddb6f85a2e7dc03c56200d",
37
+ "inputHash": "dac4b887da9913a1872ef27a446e6032ec734dd7694410f7decaef1a7c21ab09",
38
38
  "requirements": "contracts/runtime-acceptance-requirements.json",
39
39
  "gate": "node scripts/build-theme-contract.mjs --check --acceptance --evidence <path> --evidence-sha256 <hash>"
40
40
  },
package/tokens.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "packageVersion": "0.0.64",
4
- "implementationRelease": "1.0.64",
3
+ "packageVersion": "0.0.65",
4
+ "implementationRelease": "1.0.65",
5
5
  "generatedFrom": [
6
6
  "theme-config.css",
7
7
  "contracts/theme-contract.json"
8
8
  ],
9
9
  "compatibility": {
10
- "themePackageVersion": "0.0.64",
10
+ "themePackageVersion": "0.0.65",
11
11
  "manifestSchemaVersion": "1.0.0",
12
- "implementationRelease": "1.0.64",
12
+ "implementationRelease": "1.0.65",
13
13
  "tailwindVersion": "3.4.19",
14
14
  "tailwindcssPrimeuiVersion": "0.6.1",
15
15
  "primevueVersion": "4.5.5",
@@ -18,7 +18,7 @@
18
18
  "referencePrimeuixThemesVersion": "2.0.3",
19
19
  "referencePrimeuixThemesIntegrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==",
20
20
  "sourceHashes": {
21
- "themeContract": "a47fc609d0054e11f71cd21f64160c5be8b1e6ac0641329614da8c81fea87bed",
21
+ "themeContract": "6f45be1a33f15896b27923d9032fc24d4d0d11fd02dc677ea3c9571535253c42",
22
22
  "runtimeAcceptanceRequirements": "6bac23fb52a87b18b1661b5ac73747a972e07f40af6961571b5f4dd831d4cb85",
23
23
  "themeConfig": "9b484e9da76871d11edda5a555b0d0443a262acd80fb42c74c4fd78e2b23b5ae",
24
24
  "tailwindConfig": "f1e862105254f082a78823ea685e3c6dc3ff5822516b7434a1e1141c976adc1d",
@@ -39,7 +39,7 @@
39
39
  "acceptance": {
40
40
  "status": "unverified",
41
41
  "evidenceSchemaVersion": "1.0.0",
42
- "inputHash": "9fdbfaa932b0d0774028e43152812dbbb6a4150651ddb6f85a2e7dc03c56200d",
42
+ "inputHash": "dac4b887da9913a1872ef27a446e6032ec734dd7694410f7decaef1a7c21ab09",
43
43
  "requirements": "contracts/runtime-acceptance-requirements.json",
44
44
  "gate": "node scripts/build-theme-contract.mjs --check --acceptance --evidence <path> --evidence-sha256 <hash>"
45
45
  },