@qtoggle/qui 1.20.0-alpha.3 → 1.20.0-alpha.5

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.
@@ -9,8 +9,8 @@ div.qui-global-glass {
9
9
  left: 0;
10
10
  right: 0;
11
11
  bottom: 0;
12
+ /* backdrop-filter is set once below and never changes, so listing it here only ever cost a property to watch */
12
13
  transition: opacity @transition-duration ease,
13
- backdrop-filter @transition-duration linear,
14
14
  margin-top @transition-duration linear;
15
15
  opacity: 0;
16
16
  display: flex;
package/less/lists.less CHANGED
@@ -29,10 +29,15 @@ div.qui-list-child {
29
29
 
30
30
  div.qui-list-child.qui-list-item {
31
31
 
32
- /* Nothing inside an item is ever painted outside of it, since div.qui-list-child above already clips with
33
- * overflow: hidden. Containment is therefore visually inert here, but it lets the engine scope layout and style
34
- * invalidation to the single row that changed, instead of the whole list, and skip painting off-screen rows. */
35
- contain: layout style paint;
32
+ /* No `contain` here, deliberately. Layout or paint containment makes each row an independent unit for pixel
33
+ * snapping, and the separator below is a 0.0625em border -- one device pixel at the default font size, but a
34
+ * fractional one as soon as the page is zoomed or the root font size is not 16px. Snapped per row rather than
35
+ * across the list, some of those borders round away to nothing and the separator simply vanishes for a few rows.
36
+ * Reproduced at a 53.6px row pitch: 7 of 10 separators drawn with containment, 10 of 10 without. `contain: paint`
37
+ * alone and `contain: layout style` alone each do it; only `contain: style` is safe, and that buys nothing.
38
+ *
39
+ * The off-screen saving is not lost: content-visibility on the icon-label view below implies containment on that
40
+ * element, which carries no border of its own. */
36
41
 
37
42
  &.hidden {
38
43
  opacity: 0;
package/less/main-ui.less CHANGED
@@ -24,11 +24,13 @@ div.qui-main-container-glass {
24
24
  right: 0;
25
25
  bottom: 0;
26
26
  left: 0;
27
- transition: backdrop-filter @transition-duration linear;
28
- }
27
+ opacity: 0;
29
28
 
30
- div.qui-main-container-glass.visible {
29
+ /* The filter is constant and only the opacity of the filtered layer is animated. Transitioning backdrop-filter
30
+ * itself gives the compositor a different filter to apply on every frame, across the whole viewport, and nothing
31
+ * it can cache. There is no cost while hidden: VisibilityManager sets display: none once the fade has finished. */
31
32
  backdrop-filter: @modal-background-filter;
33
+ transition: opacity @transition-duration linear;
32
34
 
33
35
  /* Workaround for Firefox that doesn't support backdrop-filter property */
34
36
  @supports (-moz-appearance:none) {
@@ -36,6 +38,10 @@ div.qui-main-container-glass.visible {
36
38
  }
37
39
  }
38
40
 
41
+ div.qui-main-container-glass.visible {
42
+ opacity: 1;
43
+ }
44
+
39
45
  div.qui-pages-container {
40
46
  height: 100%;
41
47
  }
@@ -27,10 +27,36 @@ div.qui-page.visible {
27
27
  z-index: 1;
28
28
  }
29
29
 
30
- body.small-screen > div.qui-main-container > div.qui-pages-container > div.qui-page.visible,
31
- body.small-screen > div.qui-global-glass > div.qui-global-glass-container > div.qui-page.visible {
30
+ /* On a small screen a page is always exactly as wide as its container, so translateX(100%) lands in the very same
31
+ * place as left: 100% -- but on the compositor, instead of relayouting and repainting both page subtrees on every
32
+ * frame of the slide.
33
+ *
34
+ * The resting state is deliberately `transform: none` and not `translateX(0)`: any transform other than `none` makes
35
+ * the page a containing block for its `position: fixed` descendants, which would unpin the form button bar (see
36
+ * forms/form.less). Interpolating out of `none` animates perfectly well, and while a page is actually sliding, its
37
+ * button bar travelling with it is what one wants anyway.
38
+ *
39
+ * The desktop column layout deliberately stays on `left`/`width`: there a page is narrower than its container, so
40
+ * translateX(100%) would not carry it clear of the viewport, and the slide distance would have to be computed per
41
+ * page against its own width. */
42
+ body.small-screen > div.qui-main-container > div.qui-pages-container > div.qui-page,
43
+ body.small-screen > div.qui-global-glass > div.qui-global-glass-container > div.qui-page {
32
44
  width: 100% !important;
33
45
  left: 0 !important;
46
+ transform: translateX(100%);
47
+ transition: opacity @transition-duration ease-out,
48
+ transform @transition-duration ease;
49
+
50
+ &.attached {
51
+ transform: translateX(-100%);
52
+ }
53
+
54
+ /* Popups fade in place; they never slide */
55
+ &.popup,
56
+ &.visible {
57
+ transform: none;
58
+ }
59
+
34
60
  }
35
61
 
36
62
  div.qui-page.current {
@@ -20,7 +20,7 @@ div.qui-check-button {
20
20
  transition+: background @transition-duration ease,
21
21
  color @transition-duration ease,
22
22
  opacity @transition-duration ease,
23
- border @transition-duration ease;
23
+ border-color @transition-duration ease;
24
24
  .qui-focusable-widget();
25
25
 
26
26
  &:FOCUS {
@@ -4,11 +4,14 @@
4
4
  @import (reference) "common";
5
5
 
6
6
 
7
+ /* border-color, not the border shorthand: the width and style of these borders are set once and never animate, so
8
+ * naming the colour keeps the fade while leaving border-width and border-style -- the two that would cost layout --
9
+ * out of the transition entirely. */
7
10
  .qui-base-button {
8
11
  transition: background @transition-duration ease,
9
12
  color @transition-duration ease,
10
13
  opacity @transition-duration ease,
11
- border @transition-duration ease;
14
+ border-color @transition-duration ease;
12
15
  box-sizing: border-box;
13
16
  cursor: pointer;
14
17
  .noselect;
@@ -73,7 +76,7 @@
73
76
  transition+: background @transition-duration ease,
74
77
  color @transition-duration ease,
75
78
  opacity @transition-duration ease,
76
- border @transition-duration ease;
79
+ border-color @transition-duration ease;
77
80
  .qui-focusable-widget();
78
81
 
79
82
  &:FOCUS {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qtoggle/qui",
3
3
  "description": "A JavaScript UI library with batteries included.",
4
- "version": "1.20.0-alpha.3",
4
+ "version": "1.20.0-alpha.5",
5
5
  "author": {
6
6
  "name": "Calin Crisan",
7
7
  "email": "ccrisan@gmail.com"
@@ -12,6 +12,7 @@
12
12
  "keywords": [],
13
13
  "license": "Apache-2.0",
14
14
  "dependencies": {
15
+ "@babel/runtime": "^7.29.7",
15
16
  "jquery": "^3",
16
17
  "jquery-mousewheel": "*",
17
18
  "jquery-ui": "^1",
@@ -22,6 +23,7 @@
22
23
  "@babel/core": "^7.27.0",
23
24
  "@babel/eslint-parser": "^7.27.0",
24
25
  "@babel/plugin-proposal-class-properties": "^7.18.6",
26
+ "@babel/plugin-transform-runtime": "^7.29.7",
25
27
  "@babel/preset-env": "^7.27.0",
26
28
  "babel-loader": "^8",
27
29
  "css-loader": "^3",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qui-server"
3
- version = "1.20.0-alpha.3"
3
+ version = "1.20.0-alpha.5"
4
4
  description = "A fully fledged qToggle implementation written in Python"
5
5
  authors = [
6
6
  {name = "Calin Crisan", email = "ccrisan@gmail.com"},
@@ -7,18 +7,18 @@
7
7
  {% if theme_color -%}"theme_color": "{{ theme_color }}",{%- endif %}
8
8
  {% if background_color -%}"background_color": "{{ background_color }}",{%- endif %}
9
9
  "icons": [
10
- {"src": "{{ static_url }}/img/launcher-icon-16.png", "sizes": "16x16", "type": "image/png"},
11
- {"src": "{{ static_url }}/img/launcher-icon-32.png", "sizes": "32x32", "type": "image/png"},
12
- {"src": "{{ static_url }}/img/launcher-icon-36.png", "sizes": "36x36", "type": "image/png"},
13
- {"src": "{{ static_url }}/img/launcher-icon-48.png", "sizes": "48x48", "type": "image/png"},
14
- {"src": "{{ static_url }}/img/launcher-icon-64.png", "sizes": "64x64", "type": "image/png"},
15
- {"src": "{{ static_url }}/img/launcher-icon-72.png", "sizes": "72x72", "type": "image/png"},
16
- {"src": "{{ static_url }}/img/launcher-icon-96.png", "sizes": "96x96", "type": "image/png"},
17
- {"src": "{{ static_url }}/img/launcher-icon-144.png", "sizes": "144x144", "type": "image/png"},
18
- {"src": "{{ static_url }}/img/launcher-icon-192.png", "sizes": "192x192", "type": "image/png"},
19
- {"src": "{{ static_url }}/img/launcher-icon-256.png", "sizes": "256x256", "type": "image/png"},
20
- {"src": "{{ static_url }}/img/launcher-icon-384.png", "sizes": "384x384", "type": "image/png"},
21
- {"src": "{{ static_url }}/img/launcher-icon-512.png", "sizes": "512x512", "type": "image/png"}
10
+ {"src": "{{ static_url }}/img/launcher-icon-16.png?h={{ build_hash }}", "sizes": "16x16", "type": "image/png"},
11
+ {"src": "{{ static_url }}/img/launcher-icon-32.png?h={{ build_hash }}", "sizes": "32x32", "type": "image/png"},
12
+ {"src": "{{ static_url }}/img/launcher-icon-36.png?h={{ build_hash }}", "sizes": "36x36", "type": "image/png"},
13
+ {"src": "{{ static_url }}/img/launcher-icon-48.png?h={{ build_hash }}", "sizes": "48x48", "type": "image/png"},
14
+ {"src": "{{ static_url }}/img/launcher-icon-64.png?h={{ build_hash }}", "sizes": "64x64", "type": "image/png"},
15
+ {"src": "{{ static_url }}/img/launcher-icon-72.png?h={{ build_hash }}", "sizes": "72x72", "type": "image/png"},
16
+ {"src": "{{ static_url }}/img/launcher-icon-96.png?h={{ build_hash }}", "sizes": "96x96", "type": "image/png"},
17
+ {"src": "{{ static_url }}/img/launcher-icon-144.png?h={{ build_hash }}", "sizes": "144x144", "type": "image/png"},
18
+ {"src": "{{ static_url }}/img/launcher-icon-192.png?h={{ build_hash }}", "sizes": "192x192", "type": "image/png"},
19
+ {"src": "{{ static_url }}/img/launcher-icon-256.png?h={{ build_hash }}", "sizes": "256x256", "type": "image/png"},
20
+ {"src": "{{ static_url }}/img/launcher-icon-384.png?h={{ build_hash }}", "sizes": "384x384", "type": "image/png"},
21
+ {"src": "{{ static_url }}/img/launcher-icon-512.png?h={{ build_hash }}", "sizes": "512x512", "type": "image/png"}
22
22
  ],
23
23
  "start_url": ".",
24
24
  "display": "standalone"
@@ -99,7 +99,9 @@ function sendClientMessage(message, uncontrolled = false) {
99
99
  }
100
100
 
101
101
  function shouldCacheRequest(request) {
102
- return request.url.match(cacheURLRegex) && (request.method === 'GET')
102
+ /* Match on the path alone. Every asset URL carries a ?h=<build hash> query, which an extension pattern anchored
103
+ * at the end of the string would never match. */
104
+ return new URL(request.url).pathname.match(cacheURLRegex) && (request.method === 'GET')
103
105
  }
104
106
 
105
107
  function shouldCacheResponse(response) {
@@ -113,19 +115,21 @@ self.addEventListener('activate', function (event) {
113
115
 
114
116
  function deleteCache(name) {
115
117
  logDebug(`deleting cache ${name}`)
116
- caches.delete(name)
118
+ return caches.delete(name)
117
119
  }
118
120
 
119
121
  event.waitUntil(
120
- /* Become available to all pages */
121
- self.clients.claim().then(function () {
122
- /* Clear all caches starting with our app name */
122
+ Promise.all([
123
+ /* Become available to all pages */
124
+ self.clients.claim(),
125
+ /* Clear the caches left behind by other builds -- but not the one this build is about to fill, whose
126
+ * name also starts with the app name */
123
127
  caches.keys().then(function (cacheNames) {
124
128
  return Promise.all(cacheNames
125
- .filter(name => name.startsWith(`${appName}-`))
129
+ .filter(name => name.startsWith(`${appName}-`) && name !== cacheName)
126
130
  .map(name => deleteCache(name)))
127
131
  })
128
- })
132
+ ])
129
133
  )
130
134
  })
131
135
 
@@ -138,8 +142,9 @@ self.addEventListener('fetch', function (event) {
138
142
  }
139
143
 
140
144
  let url = event.request.url
141
- /* Ensure URL has build hash */
142
- if (!url.includes('?h=') && !url.includes('&h=')) {
145
+ let requestedHash = new URL(url).searchParams.get('h')
146
+ if (requestedHash == null) {
147
+ /* Ensure URL has build hash */
143
148
  if (url.includes('?')) {
144
149
  url += `&h=${buildHash}`
145
150
  }
@@ -147,6 +152,12 @@ self.addEventListener('fetch', function (event) {
147
152
  url += `?h=${buildHash}`
148
153
  }
149
154
  }
155
+ else if (requestedHash !== buildHash) {
156
+ /* A URL left over from another build, most likely requested by a page that was already open when this worker
157
+ * took over. Send it to the network untouched rather than filling this build's cache with a key that nothing
158
+ * is going to ask for again. */
159
+ return
160
+ }
150
161
 
151
162
  let request = new Request(url, event.request)
152
163
 
@@ -211,8 +211,24 @@ function makeJSRule({type, appFullPath}) {
211
211
  {
212
212
  loader: 'babel-loader',
213
213
  options: {
214
- presets: ['@babel/preset-env'],
215
- plugins: ['@babel/plugin-proposal-class-properties']
214
+ /* The entry is a <script type="module"> with no nomodule fallback, so a browser that cannot run
215
+ * ES modules never gets this far. Targeting them is the honest floor -- and it is the floor the
216
+ * markup already enforces, so it drops no browser that could previously run the app.
217
+ *
218
+ * Set here rather than in a .browserslistrc because the build compiles QUI's own sources from a
219
+ * sibling checkout outside the app's tree, where browserslist config resolution would depend on
220
+ * the working directory, and because it keeps one source of truth for both repos. */
221
+ presets: [['@babel/preset-env', {targets: {esmodules: true}}]],
222
+ plugins: [
223
+ '@babel/plugin-proposal-class-properties',
224
+ /* Import the remaining helpers from a shared runtime rather than inlining a copy of
225
+ * each into every module that needs one. useESModules defaults to false, which would pull
226
+ * the CommonJS builds of those helpers and cost webpack its tree shaking and module
227
+ * concatenation across them; "auto" takes the answer from the bundler's own declared
228
+ * capability instead of asserting it here. (Babel 8 drops the option and always
229
+ * auto-detects, so it comes out again with that upgrade.) */
230
+ ['@babel/plugin-transform-runtime', {useESModules: 'auto'}]
231
+ ]
216
232
  }
217
233
  }
218
234
  ]