@wippy-fe/theme 0.0.63 → 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.63",
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.63",
4
- "implementationRelease": "1.0.63",
3
+ "packageVersion": "0.0.65",
4
+ "implementationRelease": "1.0.65",
5
5
  "compatibility": {
6
- "themePackageVersion": "0.0.63",
6
+ "themePackageVersion": "0.0.65",
7
7
  "manifestSchemaVersion": "1.0.0",
8
- "implementationRelease": "1.0.63",
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": "6cd3dcfbee617f2e17542d27b63dac729294f2cbc3d8cc63dea7193e142508ec",
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": "ed0a419f5cb4fec1968515b5102b9795c9ca79fa0b9b464b94e9b55d160f0f93",
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.63",
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)
@@ -392,6 +472,8 @@ function setupOverlayLayer(inst: PortalInstance): void {
392
472
  // Eager: the layer is promoted to the top layer BEFORE the overlay opens, so PrimeVue
393
473
  // never measures a not-yet-promoted box.
394
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)
395
477
  // S2: stamp a unique `anchor-name` on the trigger (the placeholder's parent is the
396
478
  // component root the self-anchored families align to) and hand it to this layer.
397
479
  if (supportsAnchor()) {
@@ -419,6 +501,7 @@ function teardownOverlayLayer(inst: PortalInstance): void {
419
501
  }
420
502
  activeLayers.delete(inst.layer)
421
503
  layerObservers.get(inst.layer)?.disconnect()
504
+ unfollowTheme(inst.layer)
422
505
  inst.layer.remove()
423
506
  inst.layer = null
424
507
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "packageVersion": "0.0.63",
3
+ "packageVersion": "0.0.65",
4
4
  "compatibility": {
5
- "themePackageVersion": "0.0.63",
5
+ "themePackageVersion": "0.0.65",
6
6
  "manifestSchemaVersion": "1.0.0",
7
- "implementationRelease": "1.0.63",
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": "6cd3dcfbee617f2e17542d27b63dac729294f2cbc3d8cc63dea7193e142508ec",
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": "ed0a419f5cb4fec1968515b5102b9795c9ca79fa0b9b464b94e9b55d160f0f93",
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.63",
4
- "implementationRelease": "1.0.63",
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.63",
10
+ "themePackageVersion": "0.0.65",
11
11
  "manifestSchemaVersion": "1.0.0",
12
- "implementationRelease": "1.0.63",
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": "6cd3dcfbee617f2e17542d27b63dac729294f2cbc3d8cc63dea7193e142508ec",
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": "ed0a419f5cb4fec1968515b5102b9795c9ca79fa0b9b464b94e9b55d160f0f93",
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
  },