Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-ipyvue for openSUSE:Factory 
checked in at 2026-03-16 14:16:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ipyvue (Old)
 and      /work/SRC/openSUSE:Factory/.python-ipyvue.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ipyvue"

Mon Mar 16 14:16:56 2026 rev:11 rq:1339143 version:1.12.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ipyvue/python-ipyvue.changes      
2024-09-16 17:43:56.499204592 +0200
+++ /work/SRC/openSUSE:Factory/.python-ipyvue.new.8177/python-ipyvue.changes    
2026-03-16 14:20:06.664214579 +0100
@@ -1,0 +2,14 @@
+Sun Mar 15 19:07:43 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.12.0:
+  * fix: add a default sourceURL to aid debugging
+  * feat: add scoped CSS support
+  * fix: make scoped CSS support optional via opt-in
+
+-------------------------------------------------------------------
+Mon Sep 29 11:20:29 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 1.11.3:
+  * fix: fix: a data value of `0` turns into `{}`
+
+-------------------------------------------------------------------

Old:
----
  ipyvue-1.11.1-gh.tar.gz
  ipyvue-1.11.1.tar.gz

New:
----
  ipyvue-1.12.0-gh.tar.gz
  ipyvue-1.12.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-ipyvue.spec ++++++
--- /var/tmp/diff_new_pack.9f2jYH/_old  2026-03-16 14:20:07.332242310 +0100
+++ /var/tmp/diff_new_pack.9f2jYH/_new  2026-03-16 14:20:07.336242475 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-ipyvue
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,9 +17,9 @@
 
 
 # This is important for versions ending in .0
-%define python3dist_version 1.11.1
+%define python3dist_version 1.12
 Name:           python-ipyvue
-Version:        1.11.1
+Version:        1.12.0
 Release:        0
 Summary:        Jupyter widgets base for Vue libraries
 License:        MIT

++++++ ipyvue-1.11.1-gh.tar.gz -> ipyvue-1.12.0-gh.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/.bumpversion.cfg 
new/ipyvue-1.12.0/.bumpversion.cfg
--- old/ipyvue-1.11.1/.bumpversion.cfg  2024-05-02 10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/.bumpversion.cfg  2026-02-11 11:02:04.000000000 +0100
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 1.11.1
+current_version = 1.12.0
 commit = True
 message = chore: bump version: {current_version} → {new_version}
 tag = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/.github/workflows/unit.yml 
new/ipyvue-1.12.0/.github/workflows/unit.yml
--- old/ipyvue-1.11.1/.github/workflows/unit.yml        2024-05-02 
10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/.github/workflows/unit.yml        2026-02-11 
11:02:04.000000000 +0100
@@ -11,7 +11,7 @@
 
 jobs:
   code-quality:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v2
       - name: Install Python
@@ -26,7 +26,7 @@
         uses: rbialon/flake8-annotations@v1
 
   build:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v2
 
@@ -63,7 +63,7 @@
           if [[ $(ls -1 count | wc -l) -ne 3 ]]; then echo "Expected 4 
files/directory"; exit 1; fi
 
       - name: Upload builds
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ipyvue-dist-${{ github.run_number }}
           path: |
@@ -72,15 +72,15 @@
 
   test:
     needs: [build]
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
+        python-version: [3.8, 3.9, "3.10", "3.11"]
     steps:
       - uses: actions/checkout@v2
 
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: ipyvue-dist-${{ github.run_number }}
 
@@ -105,11 +105,11 @@
 
   ui-test:
     needs: [build]
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v2
 
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: ipyvue-dist-${{ github.run_number }}
 
@@ -131,16 +131,16 @@
 
       - name: Upload Test artifacts
         if: always()
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v4
         with:
           name: ipyvue-test-results
           path: test-results
 
   release-dry-run:
     needs: [ test,ui-test,code-quality ]
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: ipyvue-dist-${{ github.run_number }}
 
@@ -162,9 +162,9 @@
   release:
     if: startsWith(github.event.ref, 'refs/tags/v')
     needs: [release-dry-run]
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: ipyvue-dist-${{ github.run_number }}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/README.md new/ipyvue-1.12.0/README.md
--- old/ipyvue-1.11.1/README.md 2024-05-02 10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/README.md 2026-02-11 11:02:04.000000000 +0100
@@ -26,6 +26,51 @@
     $ jupyter nbextension enable --py --sys-prefix ipyvue
     $ jupyter labextension develop . --overwrite
 
+Scoped CSS Support
+------------------
+
+`<style scoped>` in `VueTemplate` templates is supported but disabled by 
default for backwards
+compatibility. When enabled, CSS rules only apply to the component's own 
elements.
+
+Enable globally via environment variable:
+
+    $ IPYVUE_SCOPED_CSS_SUPPORT=1 jupyter lab
+
+Or in Python:
+
+```python
+import ipyvue
+ipyvue.scoped_css_support = True
+```
+
+Or per widget:
+
+```python
+from ipyvue import VueTemplate
+
+class MyComponent(VueTemplate):
+    template = """
+    <template>
+        <span class="styled">Hello</span>
+    </template>
+    <style scoped>
+        .styled { color: red; }
+    </style>
+    """
+
+widget = MyComponent(scoped_css_support=True)
+```
+
+Note: The `css` trait with `scoped=True` always works, regardless of this 
setting:
+
+```python
+widget = VueTemplate(
+    template="<template><span class='x'>Hi</span></template>",
+    css=".x { color: blue; }",
+    scoped=True
+)
+```
+
 Sponsors
 --------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/examples/ScopedCSS.ipynb 
new/ipyvue-1.12.0/examples/ScopedCSS.ipynb
--- old/ipyvue-1.11.1/examples/ScopedCSS.ipynb  1970-01-01 01:00:00.000000000 
+0100
+++ new/ipyvue-1.12.0/examples/ScopedCSS.ipynb  2026-02-11 11:02:04.000000000 
+0100
@@ -0,0 +1,147 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Scoped CSS\n",
+    "\n",
+    "By default, CSS in ipyvue templates is **global** — it affects all 
elements on the page with matching selectors. Scoped CSS limits styles to the 
component that defines them.\n",
+    "\n",
+    "**How it works:** ipyvue adds a unique `data-v-*` attribute to your 
component's elements and rewrites your CSS selectors to include it (e.g., 
`.my-class` → `.my-class[data-v-abc123]`)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import ipyvue as vue\n",
+    "import ipywidgets as widgets\n",
+    "from traitlets import default"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Enable scoped CSS support\n",
+    "\n",
+    "For backwards compatibility, `<style scoped>` in templates is **disabled 
by default**. Existing code that accidentally relied on CSS leaking would break 
if we enabled it automatically.\n",
+    "\n",
+    "Enable it globally for this notebook:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Enable scoped CSS support for <style scoped> in templates\n",
+    "# Can also be set via environment variable: 
IPYVUE_SCOPED_CSS_SUPPORT=1\n",
+    "vue.scoped_css_support = True"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Without scoped CSS (the problem)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class GlobalStyle(vue.VueTemplate):\n",
+    "    @default(\"template\")\n",
+    "    def _default_template(self):\n",
+    "        return \"\"\"\n",
+    "        <template>\n",
+    "            <span class=\"demo-text\">Widget A</span>\n",
+    "        </template>\n",
+    "        <style>\n",
+    "            .demo-text { color: red; }\n",
+    "        </style>\n",
+    "        \"\"\"\n",
+    "\n",
+    "widget_b = vue.Html(tag=\"span\", children=[\"Widget B (innocent 
bystander)\"], class_=\"demo-text\")\n",
+    "\n",
+    "widgets.VBox([GlobalStyle(), widget_b])  # Both turn red!"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## With `<style scoped>`"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class ScopedStyle(vue.VueTemplate):\n",
+    "    @default(\"template\")\n",
+    "    def _default_template(self):\n",
+    "        return \"\"\"\n",
+    "        <template>\n",
+    "            <span class=\"demo-text-2\">Widget A (scoped)</span>\n",
+    "        </template>\n",
+    "        <style scoped>\n",
+    "            .demo-text-2 { color: green; }\n",
+    "        </style>\n",
+    "        \"\"\"\n",
+    "\n",
+    "widget_b = vue.Html(tag=\"span\", children=[\"Widget B (unaffected)\"], 
class_=\"demo-text-2\")\n",
+    "\n",
+    "widgets.VBox([ScopedStyle(), widget_b])  # Only Widget A is green"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Using the `css` trait with `scoped=True`\n",
+    "\n",
+    "Alternative syntax when defining CSS outside the template:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class CssTrait(vue.VueTemplate):\n",
+    "    @default(\"template\")\n",
+    "    def _default_template(self):\n",
+    "        return \"<template><span class='trait-demo'>Widget C (scoped via 
trait)</span></template>\"\n",
+    "\n",
+    "widget_c = CssTrait(css=\".trait-demo { color: blue; }\", scoped=True)\n",
+    "widget_d = vue.Html(tag=\"span\", children=[\"Widget D (unaffected)\"], 
class_=\"trait-demo\")\n",
+    "\n",
+    "widgets.VBox([widget_c, widget_d])"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "name": "python",
+   "version": "3.9.0"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/VueTemplateWidget.py 
new/ipyvue-1.12.0/ipyvue/VueTemplateWidget.py
--- old/ipyvue-1.11.1/ipyvue/VueTemplateWidget.py       2024-05-02 
10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/ipyvue/VueTemplateWidget.py       2026-02-11 
11:02:04.000000000 +0100
@@ -1,5 +1,5 @@
 import os
-from traitlets import Any, Unicode, List, Dict, Union, Instance
+from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance, default
 from ipywidgets import DOMWidget
 from ipywidgets.widgets.widget import widget_serialization
 
@@ -8,6 +8,7 @@
 from .ForceLoad import force_load_instance
 import inspect
 from importlib import import_module
+import ipyvue
 
 OBJECT_REF = "objectRef"
 FUNCTION_REF = "functionRef"
@@ -118,6 +119,14 @@
 
     css = Unicode(None, allow_none=True).tag(sync=True)
 
+    scoped = Bool(None, allow_none=True).tag(sync=True)
+
+    scoped_css_support = Bool(allow_none=False).tag(sync=True)
+
+    @default("scoped_css_support")
+    def _default_scoped_css_support(self):
+        return ipyvue.scoped_css_support
+
     methods = Unicode(None, allow_none=True).tag(sync=True)
 
     data = Unicode(None, allow_none=True).tag(sync=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/VueWidget.py 
new/ipyvue-1.12.0/ipyvue/VueWidget.py
--- old/ipyvue-1.11.1/ipyvue/VueWidget.py       2024-05-02 10:02:04.000000000 
+0200
+++ new/ipyvue-1.12.0/ipyvue/VueWidget.py       2026-02-11 11:02:04.000000000 
+0100
@@ -123,7 +123,7 @@
         dispatcher = self._event_handlers_map[event]
         # we don't call via the dispatcher, since that eats exceptions
         for callback in dispatcher.callbacks:
-            callback(self, event, data or {})
+            callback(self, event, data)
 
     def _handle_event(self, _, content, buffers):
         event = content.get("event", "")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/__init__.py 
new/ipyvue-1.12.0/ipyvue/__init__.py
--- old/ipyvue-1.11.1/ipyvue/__init__.py        2024-05-02 10:02:04.000000000 
+0200
+++ new/ipyvue-1.12.0/ipyvue/__init__.py        2026-02-11 11:02:04.000000000 
+0100
@@ -1,3 +1,5 @@
+import os
+
 from ._version import __version__
 from .Html import Html
 from .Template import Template, watch
@@ -10,6 +12,22 @@
 )
 
 
+def _parse_bool_env(key: str, default: bool = False) -> bool:
+    """Parse boolean from environment variable."""
+    val = os.environ.get(key, "").lower()
+    if val in ("1", "true", "yes", "on"):
+        return True
+    if val in ("0", "false", "no", "off"):
+        return False
+    return default
+
+
+# Global default for scoped CSS support in VueTemplate.
+# Can be set via environment variable IPYVUE_SCOPED_CSS_SUPPORT=1
+# or changed at runtime: ipyvue.scoped_css_support = True
+scoped_css_support = _parse_bool_env("IPYVUE_SCOPED_CSS_SUPPORT", False)
+
+
 def _jupyter_labextension_paths():
     return [
         {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/_version.py 
new/ipyvue-1.12.0/ipyvue/_version.py
--- old/ipyvue-1.11.1/ipyvue/_version.py        2024-05-02 10:02:04.000000000 
+0200
+++ new/ipyvue-1.12.0/ipyvue/_version.py        2026-02-11 11:02:04.000000000 
+0100
@@ -1,2 +1,2 @@
-__version__ = "1.11.1"
+__version__ = "1.12.0"
 semver = "^" + __version__
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/js/package-lock.json 
new/ipyvue-1.12.0/js/package-lock.json
--- old/ipyvue-1.11.1/js/package-lock.json      2024-05-02 10:02:04.000000000 
+0200
+++ new/ipyvue-1.12.0/js/package-lock.json      2026-02-11 11:02:04.000000000 
+0100
@@ -1,12 +1,12 @@
 {
   "name": "jupyter-vue",
-  "version": "1.11.0",
+  "version": "1.11.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "jupyter-vue",
-      "version": "1.11.0",
+      "version": "1.11.3",
       "license": "MIT",
       "dependencies": {
         "@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/js/package.json 
new/ipyvue-1.12.0/js/package.json
--- old/ipyvue-1.11.1/js/package.json   2024-05-02 10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/js/package.json   2026-02-11 11:02:04.000000000 +0100
@@ -1,6 +1,6 @@
 {
   "name": "jupyter-vue",
-  "version": "1.11.1",
+  "version": "1.12.0",
   "description": "Jupyter widgets base for Vue libraries",
   "license": "MIT",
   "author": "Mario Buikhuizen, Maarten Breddels",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/js/src/VueTemplateModel.js 
new/ipyvue-1.12.0/js/src/VueTemplateModel.js
--- old/ipyvue-1.11.1/js/src/VueTemplateModel.js        2024-05-02 
10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/js/src/VueTemplateModel.js        2026-02-11 
11:02:04.000000000 +0100
@@ -15,6 +15,7 @@
                 _model_module_version: '^0.0.3',
                 template: null,
                 css: null,
+                scoped: null,
                 methods: null,
                 data: null,
                 events: null,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/js/src/VueTemplateRenderer.js 
new/ipyvue-1.12.0/js/src/VueTemplateRenderer.js
--- old/ipyvue-1.11.1/js/src/VueTemplateRenderer.js     2024-05-02 
10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/js/src/VueTemplateRenderer.js     2026-02-11 
11:02:04.000000000 +0100
@@ -9,6 +9,73 @@
 import httpVueLoader from './httpVueLoader';
 import { TemplateModel } from './Template';
 
+function normalizeScopeId(value) {
+    return String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
+}
+
+function getScopeId(model, cssId) {
+    const base = cssId || model.cid;
+    return `data-s-${normalizeScopeId(base)}`;
+}
+
+function applyScopeId(vm, scopeId) {
+    if (!scopeId || !vm || !vm.$el) {
+        return;
+    }
+    vm.$el.setAttribute(scopeId, '');
+}
+
+function scopeStyleElement(styleElt, scopeId) {
+    const scopeSelector = `[${scopeId}]`;
+
+    function scopeRules(rules, insertRule, deleteRule) {
+        for (let i = 0; i < rules.length; ++i) {
+            const rule = rules[i];
+            if (rule.type === 1 && rule.selectorText) {
+                const scopedSelectors = [];
+                rule.selectorText.split(/\s*,\s*/).forEach((sel) => {
+                    scopedSelectors.push(`${scopeSelector} ${sel}`);
+                    const segments = sel.match(/([^ :]+)(.+)?/);
+                    if (segments) {
+                        
scopedSelectors.push(`${segments[1]}${scopeSelector}${segments[2] || ''}`);
+                    }
+                });
+                const scopedRule = scopedSelectors.join(',') + 
rule.cssText.substring(rule.selectorText.length);
+                deleteRule(i);
+                insertRule(scopedRule, i);
+            }
+            if (rule.cssRules && rule.cssRules.length && rule.insertRule && 
rule.deleteRule) {
+                scopeRules(rule.cssRules, rule.insertRule.bind(rule), 
rule.deleteRule.bind(rule));
+            }
+        }
+    }
+
+    function process() {
+        const sheet = styleElt.sheet;
+        if (!sheet) {
+            return;
+        }
+        scopeRules(sheet.cssRules, sheet.insertRule.bind(sheet), 
sheet.deleteRule.bind(sheet));
+    }
+
+    try {
+        process();
+    } catch (ex) {
+        if (typeof DOMException !== 'undefined' && ex instanceof DOMException 
&& ex.code === DOMException.INVALID_ACCESS_ERR) {
+            styleElt.sheet.disabled = true;
+            styleElt.addEventListener('load', function onStyleLoaded() {
+                styleElt.removeEventListener('load', onStyleLoaded);
+                setTimeout(() => {
+                    process();
+                    styleElt.sheet.disabled = false;
+                });
+            });
+            return;
+        }
+        throw ex;
+    }
+}
+
 export function vueTemplateRender(createElement, model, parentView) {
     return createElement(createComponentObject(model, parentView));
 }
@@ -28,10 +95,20 @@
     const isTemplateModel = model.get('template') instanceof TemplateModel;
     const templateModel = isTemplateModel ? model.get('template') : model;
     const template = templateModel.get('template');
-    const vuefile = readVueFile(template);
+    const sourceCodeFile = `VUE_TEMPLATE_SCRIPT_${model.cid}`;
+    const vuefile = readVueFile(template, sourceCodeFile);
 
     const css = model.get('css') || (vuefile.STYLE && vuefile.STYLE.content);
     const cssId = (vuefile.STYLE && vuefile.STYLE.id);
+    const scopedFromTemplate = (vuefile.STYLE && vuefile.STYLE.scoped);
+    const scoped = model.get('scoped');
+    const scopedCssSupport = model.get('scoped_css_support');
+    // If scoped trait is explicitly set, use it (for css trait with 
scoped=True/False)
+    // If scoped is not set (None), only honor <style scoped> from template if 
scoped_css_support is enabled
+    const useScoped = scoped !== null && scoped !== undefined
+        ? scoped
+        : (scopedCssSupport && scopedFromTemplate);
+    const scopeId = useScoped && css ? getScopeId(model, cssId) : null;
 
     if (css) {
         if (cssId) {
@@ -42,14 +119,32 @@
                 style.id = prefixedCssId;
                 document.head.appendChild(style);
             }
-            if (style.innerHTML !== css) {
-                style.innerHTML = css;
+            if (scopeId) {
+                if (style.innerHTML !== css || 
style.getAttribute('data-ipyvue-scope') !== scopeId) {
+                    style.innerHTML = css;
+                    scopeStyleElement(style, scopeId);
+                    style.setAttribute('data-ipyvue-scope', scopeId);
+                }
+            } else {
+                // Reset innerHTML if CSS changed or if transitioning from 
scoped to unscoped
+                // (need to reset to remove the scoped CSS rule 
transformations)
+                const wasScoped = style.getAttribute('data-ipyvue-scope');
+                if (style.innerHTML !== css || wasScoped) {
+                    style.innerHTML = css;
+                    if (wasScoped) {
+                        style.removeAttribute('data-ipyvue-scope');
+                    }
+                }
             }
         } else {
             const style = document.createElement('style');
             style.id = model.cid;
             style.innerHTML = css;
             document.head.appendChild(style);
+            if (scopeId) {
+                scopeStyleElement(style, scopeId);
+                style.setAttribute('data-ipyvue-scope', scopeId);
+            }
             parentView.once('remove', () => {
                 document.head.removeChild(style);
             });
@@ -106,15 +201,18 @@
             ? template
             : vuefile.TEMPLATE,
         beforeMount() {
+            applyScopeId(this, scopeId);
             callVueFn('beforeMount', this);
         },
         mounted() {
+            applyScopeId(this, scopeId);
             callVueFn('mounted', this);
         },
         beforeUpdate() {
             callVueFn('beforeUpdate', this);
         },
         updated() {
+            applyScopeId(this, scopeId);
             callVueFn('updated', this);
         },
         beforeDestroy() {
@@ -130,7 +228,7 @@
 function createDataMapping(model) {
     return model.keys()
         .filter(prop => !prop.startsWith('_')
-            && !['events', 'template', 'components', 'layout', 'css', 'data', 
'methods'].includes(prop))
+            && !['events', 'template', 'components', 'layout', 'css', 
'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop))
         .reduce((result, prop) => {
             result[prop] = _.cloneDeep(model.get(prop)); // 
eslint-disable-line no-param-reassign
             return result;
@@ -140,7 +238,7 @@
 function addModelListeners(model, vueModel) {
     model.keys()
         .filter(prop => !prop.startsWith('_')
-            && !['v_model', 'components', 'layout', 'css', 'data', 
'methods'].includes(prop))
+            && !['v_model', 'components', 'layout', 'css', 'scoped', 
'scoped_css_support', 'data', 'methods'].includes(prop))
         // eslint-disable-next-line no-param-reassign
         .forEach(prop => model.on(`change:${prop}`, () => {
             if (_.isEqual(model.get(prop), vueModel[prop])) {
@@ -166,7 +264,7 @@
 
 function createWatches(model, parentView, templateWatchers) {
     const modelWatchers = model.keys().filter(prop => !prop.startsWith('_')
-    && !['events', 'template', 'components', 'layout', 'css', 'data', 
'methods'].includes(prop))
+    && !['events', 'template', 'components', 'layout', 'css', 'scoped', 
'scoped_css_support', 'data', 'methods'].includes(prop))
     .reduce((result, prop) => ({
         ...result,
         [prop]: {
@@ -313,7 +411,7 @@
         }), {});
 }
 
-function readVueFile(fileContent) {
+function readVueFile(fileContent, sourceURL) {
     const component = parseComponent(fileContent, { pad: 'line' });
     const result = {};
 
@@ -322,17 +420,45 @@
     }
     if (component.script) {
         const { content } = component.script;
-        const str = content
-            .substring(content.indexOf('{'), content.length)
-            .replace('\n', ' ');
-
-        // eslint-disable-next-line no-new-func
-        result.SCRIPT = Function(`return ${str}`)();
+        try {
+            // Try the new approach first: define module and exports, then 
evaluate the whole script as if it is a commonjs module
+            const module = {
+                exports: {}
+            };
+            /*
+               Add sourceURL directive - this helps browser dev tools show 
better error locations.
+               But only add it if not already present in the content (users 
can add it themselves if they want).
+            */
+            const hasSourceURL = /\/\/#\s*sourceURL\s*=/i.test(content);
+            const contentWithScriptPath = hasSourceURL
+                ? content
+                : content + `\n//# sourceURL=${sourceURL}`;
+            const scriptFunction = new Function('module', 'exports', 
contentWithScriptPath);
+            scriptFunction(module, module.exports);
+            result.SCRIPT = module.exports;
+        } catch (error) {
+            // Fallback to the old approach for backwards compatibility
+            console.warn('Failed to evaluate Vue script and find 
module.exports, falling back to old method, please use module.exports = { ... 
}');
+            try {
+                const str = content
+                    .substring(content.indexOf('{'), content.length)
+                    .replace('\n', ' ');
+                // eslint-disable-next-line no-new-func
+                result.SCRIPT = Function(`return ${str}`)();
+            } catch (fallbackError) {
+                console.warn('Failed to evaluate Vue script with both new and 
old methods:', fallbackError);
+                /*  This is a bit like the old behaviour, except we assume the 
first error is probably correcter
+                    moreoften than not, since the old method can fail due to a 
{ being present before module.exports */
+                throw error;
+            }
+        }
     }
     if (component.styles && component.styles.length > 0) {
         const { content } = component.styles[0];
-        const { id } = component.styles[0].attrs;
-        result.STYLE = { content, id };
+        const { attrs = {} } = component.styles[0];
+        const { id } = attrs;
+        const scoped = Object.prototype.hasOwnProperty.call(attrs, 'scoped');
+        result.STYLE = { content, id, scoped };
     }
 
     return result;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/setup.py new/ipyvue-1.12.0/setup.py
--- old/ipyvue-1.11.1/setup.py  2024-05-02 10:02:04.000000000 +0200
+++ new/ipyvue-1.12.0/setup.py  2026-02-11 11:02:04.000000000 +0100
@@ -210,12 +210,10 @@
         "Intended Audience :: Developers",
         "Intended Audience :: Science/Research",
         "Topic :: Multimedia :: Graphics",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.3",
-        "Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipyvue-1.11.1/tests/ui/test_template.py 
new/ipyvue-1.12.0/tests/ui/test_template.py
--- old/ipyvue-1.11.1/tests/ui/test_template.py 2024-05-02 10:02:04.000000000 
+0200
+++ new/ipyvue-1.12.0/tests/ui/test_template.py 2026-02-11 11:02:04.000000000 
+0100
@@ -70,3 +70,163 @@
     page_session.locator("text=Click Me").click()
     page_session.locator("text=Clicked").wait_for()
     assert last_event_data == "not-an-event-object"
+
+
+class MyTemplateScript(vue.VueTemplate):
+    clicks = Int(0).tag(sync=True)
+
+    @default("template")
+    def _default_vue_template(self):
+        return """
+        <template>
+            <div @click="click">Clicked {{clicks}}</div>
+        </template>
+        <script>
+            /* test { and } in a comment, which fails in ipyvue <= 1.12.2 */
+            module.exports = {
+                methods: {
+                    click() {
+                        this.clicks += 1
+                    }
+                }
+            }
+        </script>
+        """
+
+
+class MyTemplateScriptOld(vue.VueTemplate):
+    clicks = Int(0).tag(sync=True)
+
+    @default("template")
+    def _default_vue_template(self):
+        return """
+        <template>
+            <div @click="click">Clicked {{clicks}}</div>
+        </template>
+        <script>
+            /* in ipyvue <= 1.12.2, you could put anything before
+               the first curly open */
+            foo.bar = {
+                methods: {
+                    click() {
+                        this.clicks += 1
+                    }
+                }
+            }
+        </script>
+        """
+
+
[email protected](
+    "template_class_name", ["MyTemplateScript", "MyTemplateScriptOld"]
+)
+def test_template_script(
+    ipywidgets_runner, page_session: playwright.sync_api.Page, 
template_class_name
+):
+    def kernel_code(template_class_name=template_class_name):
+        # this import is need so when this code executes in the kernel,
+        # the class is imported
+        from test_template import MyTemplateScript, MyTemplateScriptOld
+
+        template_class = {
+            "MyTemplateScript": MyTemplateScript,
+            "MyTemplateScriptOld": MyTemplateScriptOld,
+        }[template_class_name]
+
+        widget = template_class()
+        display(widget)
+
+    ipywidgets_runner(kernel_code, {"template_class_name": 
template_class_name})
+    widget = page_session.locator("text=Clicked 0")
+    widget.wait_for()
+    widget.click()
+    page_session.locator("text=Clicked 1").wait_for()
+
+
+class ScopedStyleTemplate(vue.VueTemplate):
+    @default("template")
+    def _default_vue_template(self):
+        return """
+        <template>
+            <div class="scoped-container">
+                <span id="scoped-text" class="scoped-text">Scoped text</span>
+            </div>
+        </template>
+        <style scoped>
+            .scoped-text { color: rgb(255, 0, 0); }
+        </style>
+        """
+
+
+def test_template_scoped_style(
+    ipywidgets_runner, page_session: playwright.sync_api.Page
+):
+    def kernel_code():
+        from test_template import ScopedStyleTemplate
+        import ipyvue as vue
+        import ipywidgets as widgets
+        from IPython.display import display
+
+        scoped = ScopedStyleTemplate(scoped_css_support=True)
+        unscoped = vue.Html(
+            tag="span",
+            children=["Unscoped text"],
+            class_="scoped-text",
+            attributes={"id": "unscoped-text"},
+        )
+        display(widgets.VBox([scoped, unscoped]))
+
+    ipywidgets_runner(kernel_code)
+    page_session.locator("#scoped-text").wait_for()
+    page_session.locator("#unscoped-text").wait_for()
+    scoped_color = page_session.eval_on_selector(
+        "#scoped-text", "el => getComputedStyle(el).color"
+    )
+    unscoped_color = page_session.eval_on_selector(
+        "#unscoped-text", "el => getComputedStyle(el).color"
+    )
+    assert scoped_color == "rgb(255, 0, 0)"
+    assert unscoped_color != "rgb(255, 0, 0)"
+
+
+class ScopedCssTemplate(vue.VueTemplate):
+    @default("template")
+    def _default_vue_template(self):
+        return """
+        <template>
+            <span id="scoped-css-text" class="scoped-css-text">Scoped css 
text</span>
+        </template>
+        """
+
+
+def test_template_scoped_css_trait(
+    ipywidgets_runner, page_session: playwright.sync_api.Page
+):
+    def kernel_code():
+        from test_template import ScopedCssTemplate
+        import ipyvue as vue
+        import ipywidgets as widgets
+        from IPython.display import display
+
+        scoped = ScopedCssTemplate(
+            css=".scoped-css-text { color: rgb(0, 128, 0); }", scoped=True
+        )
+        unscoped = vue.Html(
+            tag="span",
+            children=["Unscoped css text"],
+            class_="scoped-css-text",
+            attributes={"id": "unscoped-css-text"},
+        )
+        display(widgets.VBox([scoped, unscoped]))
+
+    ipywidgets_runner(kernel_code)
+    page_session.locator("#scoped-css-text").wait_for()
+    page_session.locator("#unscoped-css-text").wait_for()
+    scoped_color = page_session.eval_on_selector(
+        "#scoped-css-text", "el => getComputedStyle(el).color"
+    )
+    unscoped_color = page_session.eval_on_selector(
+        "#unscoped-css-text", "el => getComputedStyle(el).color"
+    )
+    assert scoped_color == "rgb(0, 128, 0)"
+    assert unscoped_color != "rgb(0, 128, 0)"

++++++ ipyvue-1.11.1-gh.tar.gz -> ipyvue-1.12.0.tar.gz ++++++
++++ 21900 lines of diff (skipped)

Reply via email to