stash
This commit is contained in:
parent
ed04d98bf1
commit
aa94c92000
10 changed files with 873 additions and 296 deletions
|
|
@ -48,17 +48,27 @@ toolshed:
|
||||||
```
|
```
|
||||||
|
|
||||||
- `toolshed_domain` — the **web domain**: the nginx `server_name`, Django
|
- `toolshed_domain` — the **web domain**: the nginx `server_name`, Django
|
||||||
`ALLOWED_HOSTS`, and the hostname you'll point a TLS cert at — e.g.
|
`ALLOWED_HOSTS`, and the hostname(s) you'll point a TLS cert at — e.g.
|
||||||
`toolshed.webdomain.tld`. Required, no default. This is not necessarily the
|
`toolshed.webdomain.tld`. Required, no default. May be a single domain (as
|
||||||
same as the **handle domain** your users log in with (the part after `@`
|
above) or a list, e.g. to also answer on a `www.` alias:
|
||||||
in `user@yourtoolshed.tld`) — see [DNS](#3-dns) for how those two relate.
|
```yaml
|
||||||
|
toolshed_domain:
|
||||||
|
- toolshed.webdomain.tld
|
||||||
|
- www.toolshed.webdomain.tld
|
||||||
|
```
|
||||||
|
The Let's Encrypt certificate covers all of them, named on disk after
|
||||||
|
whichever one is listed first. This is not necessarily the same as the
|
||||||
|
**handle domain** your users log in with (the part after `@` in
|
||||||
|
`user@yourtoolshed.tld`) — see [DNS](#3-dns) for how those two relate.
|
||||||
- `toolshed_handle_domain` — the **handle domain**, only needed when it's
|
- `toolshed_handle_domain` — the **handle domain**, only needed when it's
|
||||||
different from `toolshed_domain`. Omit it when the two are the same (it
|
different from `toolshed_domain`. Omit it when the two are the same (it
|
||||||
then defaults to `toolshed_domain`). It doesn't affect nginx/Django at all
|
then defaults to `toolshed_domain`). Like `toolshed_domain`, it may be a
|
||||||
(they only ever accept `toolshed_domain` as the `Host` header) — it's used
|
single domain or a list, e.g. if this deployment accepts registrations for
|
||||||
|
more than one handle domain. It doesn't affect nginx/Django at all (they
|
||||||
|
only ever accept `toolshed_domain` as the `Host` header) — it's used
|
||||||
solely to populate the `/local/domains` registration fixture (see
|
solely to populate the `/local/domains` registration fixture (see
|
||||||
`toolshed_register_domains` in `playbook.yml`); publishing the SRV record is a
|
`toolshed_register_domains` in `playbook.yml`); publishing the SRV record for
|
||||||
separate, manual DNS step either way.
|
each handle domain is a separate, manual DNS step either way.
|
||||||
- `toolshed_repo_url` — the git remote the playbook checks out and builds
|
- `toolshed_repo_url` — the git remote the playbook checks out and builds
|
||||||
from. Required, no default.
|
from. Required, no default.
|
||||||
- `toolshed_version` — the branch, tag or commit to check out and build.
|
- `toolshed_version` — the branch, tag or commit to check out and build.
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,21 @@ toolshed:
|
||||||
ansible_user: deploy
|
ansible_user: deploy
|
||||||
# toolshed_domain is the "web domain" - see the README's DNS section
|
# toolshed_domain is the "web domain" - see the README's DNS section
|
||||||
# for how this relates to the separate "handle domain" your users
|
# for how this relates to the separate "handle domain" your users
|
||||||
# log in with (user@yourtoolshed.tld).
|
# log in with (user@yourtoolshed.tld). May be a single domain (as
|
||||||
|
# here) or a list, e.g. to also answer on a "www." alias:
|
||||||
|
# toolshed_domain:
|
||||||
|
# - toolshed.webdomain.tld
|
||||||
|
# - www.toolshed.webdomain.tld
|
||||||
|
# The Let's Encrypt certificate is requested for all of them, named
|
||||||
|
# after whichever one is listed first.
|
||||||
toolshed_domain: toolshed.webdomain.tld
|
toolshed_domain: toolshed.webdomain.tld
|
||||||
# Optional - only needed if the handle domain differs from the web
|
# Optional - only needed if the handle domain differs from the web
|
||||||
# domain above. Omit it entirely when they're the same.
|
# domain above. Omit it entirely when they're the same. Like
|
||||||
|
# toolshed_domain, this may be a single domain or a list, e.g. if this
|
||||||
|
# deployment accepts registrations for more than one handle domain:
|
||||||
|
# toolshed_handle_domain:
|
||||||
|
# - yourtoolshed.tld
|
||||||
|
# - alt.yourtoolshed.tld
|
||||||
toolshed_handle_domain: yourtoolshed.tld
|
toolshed_handle_domain: yourtoolshed.tld
|
||||||
toolshed_repo_url: git@example.com:your-org/toolshed.git
|
toolshed_repo_url: git@example.com:your-org/toolshed.git
|
||||||
# Optional - branch, tag or commit to deploy. Defaults to "stable".
|
# Optional - branch, tag or commit to deploy. Defaults to "stable".
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@
|
||||||
# at a time, never the whole "toolshed" group in one run):
|
# at a time, never the whole "toolshed" group in one run):
|
||||||
# ansible-playbook -i inventory.yml playbook.yml --limit my-server
|
# ansible-playbook -i inventory.yml playbook.yml --limit my-server
|
||||||
#
|
#
|
||||||
# toolshed_repo_url, toolshed_domain, toolshed_handle_domain (optional),
|
# toolshed_repo_url, toolshed_domain, toolshed_handle_domain (optional,
|
||||||
# toolshed_version (optional, defaults to "stable"), behind_tls_proxy and
|
# either may be a single domain or a list of domains), toolshed_version
|
||||||
|
# (optional, defaults to "stable"), behind_tls_proxy and
|
||||||
# toolshed_letsencrypt_email (required unless behind_tls_proxy is true) are
|
# toolshed_letsencrypt_email (required unless behind_tls_proxy is true) are
|
||||||
# per-deployment and must be set as host_vars in inventory.yml (copy
|
# per-deployment and must be set as host_vars in inventory.yml (copy
|
||||||
# inventory.example.yml) rather than here or via -e, so that each host in
|
# inventory.example.yml) rather than here or via -e, so that each host in
|
||||||
|
|
@ -53,11 +54,17 @@
|
||||||
# proxying to gunicorn for every asset request.
|
# proxying to gunicorn for every asset request.
|
||||||
toolshed_static_dir: /var/www/toolshed-static
|
toolshed_static_dir: /var/www/toolshed-static
|
||||||
# Domain(s) this server accepts registrations for (the "handle domain" -
|
# Domain(s) this server accepts registrations for (the "handle domain" -
|
||||||
# see the README's DNS section). Served as a static /local/domains
|
# see the README's DNS section). toolshed_handle_domain may be a single
|
||||||
# fixture that the frontend's registration/pairing forms fetch to
|
# domain or a list; when unset it falls back to toolshed_domain (whole
|
||||||
# populate their domain dropdown (frontend/src/views/Register.vue,
|
# list, if that's a list too). Served as a static /local/domains fixture
|
||||||
# Pairing.vue) - without it that dropdown is just empty.
|
# that the frontend's registration/pairing forms fetch to populate their
|
||||||
toolshed_register_domains: "{{ [toolshed_handle_domain | default(toolshed_domain)] | unique }}"
|
# domain dropdown (frontend/src/views/Register.vue, Pairing.vue) -
|
||||||
|
# without it that dropdown is just empty.
|
||||||
|
toolshed_handle_domain_or_default: "{{ toolshed_handle_domain | default(toolshed_domain) }}"
|
||||||
|
toolshed_register_domains: >-
|
||||||
|
{{ ([toolshed_handle_domain_or_default]
|
||||||
|
if toolshed_handle_domain_or_default is string
|
||||||
|
else toolshed_handle_domain_or_default) | unique }}
|
||||||
# DoH resolvers the frontend falls back to for SRV lookups when it has
|
# DoH resolvers the frontend falls back to for SRV lookups when it has
|
||||||
# no cached preference yet, served as a static /local/dns fixture. These
|
# no cached preference yet, served as a static /local/dns fixture. These
|
||||||
# match the frontend's own hardcoded fallback (frontend/src/dns.js), so
|
# match the frontend's own hardcoded fallback (frontend/src/dns.js), so
|
||||||
|
|
@ -90,8 +97,18 @@
|
||||||
# and doesn't necessarily have an A record pointing at this host at all
|
# and doesn't necessarily have an A record pointing at this host at all
|
||||||
# (see the README's DNS section), so it can't reliably serve an HTTP-01
|
# (see the README's DNS section), so it can't reliably serve an HTTP-01
|
||||||
# challenge or ever show up as this nginx's Host header.
|
# challenge or ever show up as this nginx's Host header.
|
||||||
|
#
|
||||||
|
# toolshed_domain may be a single domain or a list (e.g. a bare domain
|
||||||
|
# plus a "www." alias). certbot names the Let's Encrypt certificate's
|
||||||
|
# live/ directory after whichever domain is passed first via -d, so
|
||||||
|
# toolshed_hostnames[0] (below) is used wherever the playbook needs to
|
||||||
|
# reference that directory by name.
|
||||||
|
toolshed_domain_checked: >-
|
||||||
|
{{ toolshed_domain | mandatory('toolshed_domain must be set as a host_var for ' ~ inventory_hostname) }}
|
||||||
toolshed_hostnames: >-
|
toolshed_hostnames: >-
|
||||||
{{ [toolshed_domain | mandatory('toolshed_domain must be set as a host_var for ' ~ inventory_hostname)] }}
|
{{ ([toolshed_domain_checked]
|
||||||
|
if toolshed_domain_checked is string
|
||||||
|
else toolshed_domain_checked) | unique }}
|
||||||
# Generated once per host on the controller and reused on every
|
# Generated once per host on the controller and reused on every
|
||||||
# subsequent run against that host, keyed by inventory_hostname so
|
# subsequent run against that host, keyed by inventory_hostname so
|
||||||
# separate deployments never end up sharing a Django SECRET_KEY.
|
# separate deployments never end up sharing a Django SECRET_KEY.
|
||||||
|
|
@ -243,8 +260,8 @@
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
server_name {{ toolshed_hostnames | join(' ') }};
|
server_name {{ toolshed_hostnames | join(' ') }};
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ toolshed_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ toolshed_hostnames[0] }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ toolshed_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ toolshed_hostnames[0] }}/privkey.pem;
|
||||||
|
|
||||||
client_max_body_size 128M;
|
client_max_body_size 128M;
|
||||||
root {{ toolshed_dist_dir }};
|
root {{ toolshed_dist_dir }};
|
||||||
|
|
@ -330,19 +347,13 @@
|
||||||
# until reloaded.
|
# until reloaded.
|
||||||
notify: reload nginx
|
notify: reload nginx
|
||||||
|
|
||||||
# Owned by ansible_user rather than www-data up front: the frontend dist
|
|
||||||
# sync below pushes files over a plain rsync-over-ssh connection as
|
|
||||||
# ansible_user (synchronize shells out to the local rsync binary, which
|
|
||||||
# opens its own ssh session - it doesn't go through Ansible's become),
|
|
||||||
# so that account needs write access here first. "Fix ownership of
|
|
||||||
# exported frontend build" resets this to www-data (via become) right
|
|
||||||
# after the sync completes.
|
|
||||||
- name: Create frontend static output directory
|
- name: Create frontend static output directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ toolshed_dist_dir }}"
|
path: "{{ toolshed_dist_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
owner: "{{ ansible_user }}"
|
owner: "www-data"
|
||||||
mode: "0755"
|
group: "www-data"
|
||||||
|
mode: "0750"
|
||||||
|
|
||||||
- name: Create backend static output directory
|
- name: Create backend static output directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
@ -350,7 +361,7 @@
|
||||||
state: directory
|
state: directory
|
||||||
owner: www-data
|
owner: www-data
|
||||||
group: www-data
|
group: www-data
|
||||||
mode: "0755"
|
mode: "0750"
|
||||||
|
|
||||||
- name: Write backend environment file
|
- name: Write backend environment file
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
|
|
@ -570,7 +581,7 @@
|
||||||
|
|
||||||
- name: Check for an existing Let's Encrypt certificate
|
- name: Check for an existing Let's Encrypt certificate
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/letsencrypt/live/{{ toolshed_domain }}/fullchain.pem"
|
path: "/etc/letsencrypt/live/{{ toolshed_hostnames[0] }}/fullchain.pem"
|
||||||
register: toolshed_cert
|
register: toolshed_cert
|
||||||
when: not (behind_tls_proxy | default(false) | bool)
|
when: not (behind_tls_proxy | default(false) | bool)
|
||||||
|
|
||||||
|
|
@ -633,7 +644,7 @@
|
||||||
|
|
||||||
- name: Re-check the certificate now that certbot has run
|
- name: Re-check the certificate now that certbot has run
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/letsencrypt/live/{{ toolshed_domain }}/fullchain.pem"
|
path: "/etc/letsencrypt/live/{{ toolshed_hostnames[0] }}/fullchain.pem"
|
||||||
register: toolshed_cert
|
register: toolshed_cert
|
||||||
when: not (behind_tls_proxy | default(false) | bool)
|
when: not (behind_tls_proxy | default(false) | bool)
|
||||||
|
|
||||||
|
|
|
||||||
491
frontend/package-lock.json
generated
491
frontend/package-lock.json
generated
|
|
@ -5,31 +5,30 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": {
|
"@babel/helper-string-parser": {
|
||||||
"version": "7.24.8",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ=="
|
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="
|
||||||
},
|
},
|
||||||
"@babel/helper-validator-identifier": {
|
"@babel/helper-validator-identifier": {
|
||||||
"version": "7.24.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w=="
|
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="
|
||||||
},
|
},
|
||||||
"@babel/parser": {
|
"@babel/parser": {
|
||||||
"version": "7.25.6",
|
"version": "7.29.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||||
"integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
|
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.25.6"
|
"@babel/types": "^7.29.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/types": {
|
"@babel/types": {
|
||||||
"version": "7.25.6",
|
"version": "7.29.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||||
"integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
|
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-string-parser": "^7.24.8",
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
"@babel/helper-validator-identifier": "^7.24.7",
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
"to-fast-properties": "^2.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm": {
|
"@esbuild/android-arm": {
|
||||||
|
|
@ -201,9 +200,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@jridgewell/sourcemap-codec": {
|
"@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
||||||
},
|
},
|
||||||
"@leichtgewicht/base64-codec": {
|
"@leichtgewicht/base64-codec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -250,33 +249,30 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@tootallnate/once": {
|
"@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
|
||||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
"integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/chai": {
|
"@types/chai": {
|
||||||
"version": "4.3.19",
|
"version": "4.3.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz",
|
||||||
"integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==",
|
"integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/chai-subset": {
|
"@types/chai-subset": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz",
|
||||||
"integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==",
|
"integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {
|
|
||||||
"@types/chai": "*"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "22.5.4",
|
"version": "26.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vitejs/plugin-vue": {
|
"@vitejs/plugin-vue": {
|
||||||
|
|
@ -455,15 +451,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "8.12.1",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
"acorn-walk": {
|
||||||
"version": "8.3.4",
|
"version": "8.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"acorn": "^8.11.0"
|
"acorn": "^8.11.0"
|
||||||
|
|
@ -479,15 +475,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
|
|
@ -541,9 +537,9 @@
|
||||||
"integrity": "sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw=="
|
"integrity": "sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw=="
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|
@ -569,6 +565,16 @@
|
||||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"chai": {
|
"chai": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
||||||
|
|
@ -656,9 +662,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"confbox": {
|
"confbox": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||||
"integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==",
|
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"config-chain": {
|
"config-chain": {
|
||||||
|
|
@ -672,9 +678,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
|
|
@ -692,9 +698,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||||
},
|
},
|
||||||
"data-urls": {
|
"data-urls": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
|
@ -717,18 +723,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"decimal.js": {
|
"decimal.js": {
|
||||||
"version": "10.4.3",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"deep-eql": {
|
"deep-eql": {
|
||||||
|
|
@ -767,6 +773,17 @@
|
||||||
"webidl-conversions": "^7.0.0"
|
"webidl-conversions": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eastasianwidth": {
|
"eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
@ -774,14 +791,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"editorconfig": {
|
"editorconfig": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
|
||||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
"integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@one-ini/wasm": "0.1.1",
|
"@one-ini/wasm": "0.1.1",
|
||||||
"commander": "^10.0.0",
|
"commander": "^10.0.0",
|
||||||
"minimatch": "9.0.1",
|
"minimatch": "^9.0.1",
|
||||||
"semver": "^7.5.3"
|
"semver": "^7.5.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -796,6 +813,39 @@
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
},
|
},
|
||||||
|
"es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"es-object-atoms": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
|
|
@ -856,24 +906,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreground-child": {
|
"foreground-child": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"cross-spawn": "^7.0.0",
|
"cross-spawn": "^7.0.6",
|
||||||
"signal-exit": "^4.0.1"
|
"signal-exit": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-data": {
|
"form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"mime-types": "^2.1.12"
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.4",
|
||||||
|
"mime-types": "^2.1.35"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fsevents": {
|
"fsevents": {
|
||||||
|
|
@ -883,16 +935,50 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"get-func-name": {
|
"get-func-name": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"foreground-child": "^3.1.0",
|
"foreground-child": "^3.1.0",
|
||||||
|
|
@ -901,17 +987,6 @@
|
||||||
"minipass": "^7.1.2",
|
"minipass": "^7.1.2",
|
||||||
"package-json-from-dist": "^1.0.0",
|
"package-json-from-dist": "^1.0.0",
|
||||||
"path-scurry": "^1.11.1"
|
"path-scurry": "^1.11.1"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"minimatch": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"glob-parent": {
|
"glob-parent": {
|
||||||
|
|
@ -923,6 +998,36 @@
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hasown": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"html-encoding-sniffer": {
|
"html-encoding-sniffer": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||||
|
|
@ -963,9 +1068,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"immutable": {
|
"immutable": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz",
|
||||||
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
|
"integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
|
|
@ -1038,22 +1143,22 @@
|
||||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
|
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
|
||||||
},
|
},
|
||||||
"js-beautify": {
|
"js-beautify": {
|
||||||
"version": "1.15.1",
|
"version": "1.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
|
||||||
"integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
|
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"config-chain": "^1.1.13",
|
"config-chain": "^1.1.13",
|
||||||
"editorconfig": "^1.0.4",
|
"editorconfig": "^1.0.4",
|
||||||
"glob": "^10.3.3",
|
"glob": "^10.4.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"nopt": "^7.2.0"
|
"nopt": "^7.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"js-cookie": {
|
"js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"js-nacl": {
|
"js-nacl": {
|
||||||
|
|
@ -1105,9 +1210,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"loupe": {
|
"loupe": {
|
||||||
|
|
@ -1133,6 +1238,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"md5-hex": {
|
"md5-hex": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
|
||||||
|
|
@ -1158,30 +1269,38 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"mlly": {
|
"mlly": {
|
||||||
"version": "1.7.1",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||||
"integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
|
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"acorn": "^8.11.3",
|
"acorn": "^8.16.0",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^2.0.3",
|
||||||
"pkg-types": "^1.1.1",
|
"pkg-types": "^1.3.1",
|
||||||
"ufo": "^1.5.3"
|
"ufo": "^1.6.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
|
|
@ -1196,9 +1315,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="
|
||||||
},
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "7.2.1",
|
"version": "7.2.1",
|
||||||
|
|
@ -1216,9 +1335,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"nwsapi": {
|
"nwsapi": {
|
||||||
"version": "2.2.12",
|
"version": "2.2.24",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||||
"integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==",
|
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
|
|
@ -1231,18 +1350,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"package-json-from-dist": {
|
"package-json-from-dist": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"parse5": {
|
"parse5": {
|
||||||
"version": "7.1.2",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"entities": "^4.4.0"
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"path-key": {
|
"path-key": {
|
||||||
|
|
@ -1279,20 +1406,28 @@
|
||||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||||
},
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"pkg-types": {
|
"pkg-types": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||||
"integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==",
|
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"confbox": "^0.1.7",
|
"confbox": "^0.1.8",
|
||||||
"mlly": "^1.7.1",
|
"mlly": "^1.7.4",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^2.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"popper.js": {
|
"popper.js": {
|
||||||
|
|
@ -1342,10 +1477,13 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"psl": {
|
"psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
|
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
|
@ -1381,9 +1519,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "3.29.4",
|
"version": "3.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz",
|
||||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
"integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
|
|
@ -1422,9 +1560,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.6.3",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
|
|
@ -1466,9 +1604,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"std-env": {
|
"std-env": {
|
||||||
"version": "3.7.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
|
|
@ -1517,12 +1655,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "7.1.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"strip-ansi-cjs": {
|
"strip-ansi-cjs": {
|
||||||
|
|
@ -1581,11 +1719,6 @@
|
||||||
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
|
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-fast-properties": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
|
|
||||||
},
|
|
||||||
"to-regex-range": {
|
"to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|
@ -1623,15 +1756,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ufo": {
|
"ufo": {
|
||||||
"version": "1.5.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
|
||||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"undici-types": {
|
"undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
|
|
@ -1742,9 +1875,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-component-type-helpers": {
|
"vue-component-type-helpers": {
|
||||||
"version": "2.1.6",
|
"version": "2.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
|
||||||
"integrity": "sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==",
|
"integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"vue-multiselect": {
|
"vue-multiselect": {
|
||||||
|
|
@ -1899,9 +2032,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.18.0",
|
"version": "8.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
|
|
@ -1917,9 +2050,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"yocto-queue": {
|
"yocto-queue": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||||
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,51 @@
|
||||||
<h5 class="card-title mb-0">Label layout</h5>
|
<h5 class="card-title mb-0">Label layout</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="mb-3 btn-group" role="group">
|
||||||
|
<input type="radio" class="btn-check" id="layout-filter-all"
|
||||||
|
autocomplete="off" value="all" v-model="activeTag">
|
||||||
|
<label class="btn btn-outline-secondary" for="layout-filter-all">All</label>
|
||||||
|
<template v-for="tag in labelTags" :key="tag">
|
||||||
|
<input type="radio" class="btn-check" :id="'layout-filter-' + tag"
|
||||||
|
autocomplete="off" :value="tag" v-model="activeTag">
|
||||||
|
<label class="btn btn-outline-secondary" :for="'layout-filter-' + tag">
|
||||||
|
{{ tagLabel(tag) }}
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div class="template-grid d-flex flex-wrap align-items-start">
|
<div class="template-grid d-flex flex-wrap align-items-start">
|
||||||
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
|
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
|
||||||
:class="{'template-option-disabled': !isSelectable(t)}"
|
:class="{'template-option-disabled': !isSelectable(t)}"
|
||||||
:title="unavailableReason(t)"
|
:title="unavailableReason(t)"
|
||||||
role="button" @click="isSelectable(t) && $emit('input', t.id)">
|
role="button" @click="isSelectable(t) && $emit('input', t.id)">
|
||||||
|
<div class="template-thumb-wrap">
|
||||||
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
||||||
class="img-thumbnail template-thumb-canvas"
|
class="img-thumbnail template-thumb-canvas"
|
||||||
:class="{'border-primary': value === t.id}"></canvas>
|
:class="{'border-primary': value === t.id}"></canvas>
|
||||||
|
<span v-if="t.id in failed" class="template-thumb-warning" :title="failed[t.id].message">
|
||||||
|
{{ failed[t.id].short }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="t.id in warned" class="template-thumb-warning template-thumb-warning-soft"
|
||||||
|
:title="warned[t.id].map(w => w.message).join(' ')">
|
||||||
|
{{ warned[t.id].map(w => w.short).join(", ") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="small"
|
<div class="small"
|
||||||
:class="{'fw-bold text-primary': value === t.id}">
|
:class="{'fw-bold text-primary': value === t.id}">
|
||||||
{{ t.name }}
|
{{ t.name }}
|
||||||
|
<span v-for="tag in t.tags" :key="tag" class="badge bg-secondary">{{ tagLabel(tag) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted">{{ t.description }}</div>
|
<div class="small text-muted">{{ t.description }}</div>
|
||||||
|
<!-- Debugging aid: every QR-family/text leaf's raw size numbers, shown regardless
|
||||||
|
of whether either tripped a warning above - see label.js's qrInfo/textInfo. -->
|
||||||
|
<div v-if="t.id in qrInfo || t.id in textInfo" class="small text-secondary">
|
||||||
|
<div v-for="(info, i) in qrInfo[t.id]" :key="'qr' + i">
|
||||||
|
mm-per-mod: {{ info.moduleMm.toFixed(2) }}, px-per-mod: {{ info.scale }}
|
||||||
|
</div>
|
||||||
|
<div v-for="(info, i) in textInfo[t.id]" :key="'text' + i">
|
||||||
|
text-mm: {{ info.fontMm.toFixed(2) }}, text-px: {{ info.fontPx.toFixed(1) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,6 +70,16 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-option .badge {
|
||||||
|
font-size: .65rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-thumb-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.template-thumb-canvas {
|
.template-thumb-canvas {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -47,11 +89,44 @@
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Corner badge flagging *why* a layout that's otherwise available (fields filled in) still
|
||||||
|
failed to render - e.g. label.js's "bigger code than the tape allows" or "doesn't fit on this
|
||||||
|
tape" errors (see redraw()'s catch). Shows the short label directly (e.g. "too big") rather than
|
||||||
|
just an icon, so the reason reads at a glance; the full sentence is still the title tooltip. Not
|
||||||
|
shown for the more common "fields not filled in yet" case (see isSelectable/unavailableReason)
|
||||||
|
since that's already conveyed by the greyed-out thumbnail. */
|
||||||
|
.template-thumb-warning {
|
||||||
|
position: absolute;
|
||||||
|
top: .35rem;
|
||||||
|
right: .35rem;
|
||||||
|
max-width: calc(100% - .7rem);
|
||||||
|
padding: .15rem .45rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(220, 53, 69, .9);
|
||||||
|
color: #fff;
|
||||||
|
font-size: .7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Soft variant for a template that rendered fine but tripped one of label.js's scan-reliability
|
||||||
|
thresholds (MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM) - still selectable,
|
||||||
|
just flagged, so amber rather than the hard-failure badge's red. */
|
||||||
|
.template-thumb-warning-soft {
|
||||||
|
background: rgba(255, 193, 7, .9);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
import {drawLabel, drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
||||||
import {LABEL_TEMPLATES, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
import {LABEL_TEMPLATES, LABEL_TAGS, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LabelLayoutPreview",
|
name: "LabelLayoutPreview",
|
||||||
|
|
@ -73,6 +148,15 @@ export default {
|
||||||
recentTemplateIds: {
|
recentTemplateIds: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
// The connected printer's tape/resolution (see label.js's tapeFromStatus), or null when
|
||||||
|
// no printer is connected. When present, redraw() renders every thumbnail with drawLabel
|
||||||
|
// at this tape's real dpi/printArea/length instead of drawFallbackLabel's fixed reference
|
||||||
|
// - so the grid's px/mm-per-module numbers and any length-limit failures reflect what
|
||||||
|
// would actually come out of the connected printer.
|
||||||
|
tape: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
|
|
@ -82,30 +166,67 @@ export default {
|
||||||
emits: ["input"],
|
emits: ["input"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Keyed by template id, holding the error message from its most recent redraw()
|
// Keyed by template id, holding the {short, message} error from its most recent
|
||||||
// failure (e.g. text too long, or too many QR modules for the thumbnail's fixed
|
// redraw() failure (e.g. text too long, or too many QR modules for the thumbnail's
|
||||||
// reference size - see label.js's snapQrToCrispSize). Absent, not just falsy, for a
|
// fixed reference size - see label.js's snapQrToCrispSize/encodeQr). Absent, not just
|
||||||
// template that last drew fine, so `t.id in failed` matches "has a message".
|
// falsy, for a template that last drew fine, so `t.id in failed` matches "has one".
|
||||||
failed: {},
|
failed: {},
|
||||||
|
// Keyed by template id, holding the array of {short, message} scan-reliability
|
||||||
|
// warnings (see label.js's drawFallbackLabel) from its most recent successful
|
||||||
|
// redraw() - present only when that render tripped
|
||||||
|
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||||
|
warned: {},
|
||||||
|
// Keyed by template id, holding every QR-family leaf's raw {scale, moduleMm} from its
|
||||||
|
// most recent successful redraw(), regardless of whether it warned - a debugging aid
|
||||||
|
// shown unconditionally below each thumbnail.
|
||||||
|
qrInfo: {},
|
||||||
|
// Same idea as qrInfo, but every text leaf's raw {fontPx, fontMm} (see label.js's
|
||||||
|
// checkTextSizes).
|
||||||
|
textInfo: {},
|
||||||
|
// Which LABEL_TAGS entry (or "all") the grid below is narrowed to.
|
||||||
|
activeTag: "all",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
labelTags() {
|
||||||
|
return LABEL_TAGS;
|
||||||
|
},
|
||||||
// LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
|
// LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
|
||||||
// first), everything else following in its original order.
|
// first, everything else following in its original order), narrowed to activeTag, then
|
||||||
|
// with anything currently in `failed` (not printable - see redraw()'s catch) stably
|
||||||
|
// sorted to the back regardless of recency/tag order, so real errors don't crowd out
|
||||||
|
// layouts that actually work.
|
||||||
labelTemplates() {
|
labelTemplates() {
|
||||||
const recent = this.recentTemplateIds
|
const recent = this.recentTemplateIds
|
||||||
.map(id => LABEL_TEMPLATES.find(t => t.id === id))
|
.map(id => LABEL_TEMPLATES.find(t => t.id === id))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const recentIds = new Set(recent.map(t => t.id));
|
const recentIds = new Set(recent.map(t => t.id));
|
||||||
return [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
const ordered = [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
||||||
|
const filtered = this.activeTag === "all" ? ordered : ordered.filter(t => t.tags?.includes(this.activeTag));
|
||||||
|
return [...filtered].sort((a, b) => (a.id in this.failed ? 1 : 0) - (b.id in this.failed ? 1 : 0));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
fields() {
|
fields() {
|
||||||
this.redraw();
|
this.redraw();
|
||||||
|
},
|
||||||
|
// Switching filters mounts fresh canvas elements for thumbnails hidden until now (see
|
||||||
|
// labelTemplates); nextTick waits for those refs to land before drawing into them.
|
||||||
|
activeTag() {
|
||||||
|
this.$nextTick(() => this.redraw());
|
||||||
|
},
|
||||||
|
// Printer connect/disconnect/switch - every thumbnail's canvas already exists regardless
|
||||||
|
// of tape (unlike Print.vue's own tape-fed preview), so no flush:'post' is needed here.
|
||||||
|
tape() {
|
||||||
|
this.redraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// "internal" -> "Internal", for both the filter buttons and each thumbnail's badge.
|
||||||
|
tagLabel(tag) {
|
||||||
|
return tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||||
|
},
|
||||||
|
|
||||||
setTemplateCanvasRef(id, el) {
|
setTemplateCanvasRef(id, el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.templateCanvases[id] = el;
|
this.templateCanvases[id] = el;
|
||||||
|
|
@ -130,12 +251,25 @@ export default {
|
||||||
if (!this.isAvailable(t)) {
|
if (!this.isAvailable(t)) {
|
||||||
return "Not available - fill in the fields this layout needs above.";
|
return "Not available - fill in the fields this layout needs above.";
|
||||||
}
|
}
|
||||||
return this.failed[t.id] ?? "";
|
return this.failed[t.id]?.message ?? "";
|
||||||
},
|
},
|
||||||
|
|
||||||
// Live per-template thumbnails. Always uses the content-fit fallback renderer (not the
|
// .template-thumb-canvas's object-fit:contain scales the just-drawn bitmap into its fixed
|
||||||
// tape-fed one) regardless of printer connection - illustrative previews via CSS
|
// CSS box (getBoundingClientRect, unlike canvas.width/height, reflects that box - object-fit
|
||||||
// object-fit, not the to-be-printed-accurate canvas the main preview is.
|
// doesn't change it); when that's a magnification, switch to nearest-neighbor so the
|
||||||
|
// print-accurate pixel edges stay crisp instead of blurring, same call Print.vue's own
|
||||||
|
// fitZoom makes for its explicitly-sized preview.
|
||||||
|
applyImageRendering(canvas) {
|
||||||
|
const {width: boxWidth, height: boxHeight} = canvas.getBoundingClientRect();
|
||||||
|
const scale = Math.min(boxWidth / canvas.width, boxHeight / canvas.height);
|
||||||
|
canvas.style.imageRendering = scale >= 1 ? "pixelated" : "auto";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Live per-template thumbnails. Renders each with drawLabel at the connected printer's
|
||||||
|
// real tape/resolution when one's connected (see the `tape` prop), so the numbers/errors
|
||||||
|
// shown match what would actually print; falls back to drawFallbackLabel's fixed
|
||||||
|
// reference size otherwise. Either way, still illustrative previews via CSS object-fit,
|
||||||
|
// not the to-be-printed-accurate canvas Print.vue's own main preview is.
|
||||||
redraw() {
|
redraw() {
|
||||||
for (const t of LABEL_TEMPLATES) {
|
for (const t of LABEL_TEMPLATES) {
|
||||||
const canvas = this.templateCanvases[t.id];
|
const canvas = this.templateCanvases[t.id];
|
||||||
|
|
@ -147,18 +281,42 @@ export default {
|
||||||
canvas.width = 1;
|
canvas.width = 1;
|
||||||
canvas.height = 1;
|
canvas.height = 1;
|
||||||
delete this.failed[t.id];
|
delete this.failed[t.id];
|
||||||
|
delete this.warned[t.id];
|
||||||
|
delete this.qrInfo[t.id];
|
||||||
|
delete this.textInfo[t.id];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
drawFallbackLabel(canvas, content, "along");
|
const {warnings, qrInfo, textInfo} = this.tape
|
||||||
|
? drawLabel(canvas, this.tape, content, "along")
|
||||||
|
: drawFallbackLabel(canvas, content, "along");
|
||||||
|
this.applyImageRendering(canvas);
|
||||||
delete this.failed[t.id];
|
delete this.failed[t.id];
|
||||||
|
if (warnings.length) {
|
||||||
|
this.warned[t.id] = warnings;
|
||||||
|
} else {
|
||||||
|
delete this.warned[t.id];
|
||||||
|
}
|
||||||
|
if (qrInfo.length) {
|
||||||
|
this.qrInfo[t.id] = qrInfo;
|
||||||
|
} else {
|
||||||
|
delete this.qrInfo[t.id];
|
||||||
|
}
|
||||||
|
if (textInfo.length) {
|
||||||
|
this.textInfo[t.id] = textInfo;
|
||||||
|
} else {
|
||||||
|
delete this.textInfo[t.id];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Grey the thumbnail out (see isSelectable/unavailableReason) rather than
|
// Grey the thumbnail out (see isSelectable/unavailableReason) rather than
|
||||||
// leaving it blank - selecting a template that can't render here would only
|
// leaving it blank - selecting a template that can't render here would only
|
||||||
// hand Print.vue's own redraw the exact same failure.
|
// hand Print.vue's own redraw the exact same failure.
|
||||||
canvas.width = 1;
|
canvas.width = 1;
|
||||||
canvas.height = 1;
|
canvas.height = 1;
|
||||||
this.failed[t.id] = e.message;
|
this.failed[t.id] = {short: e.short ?? "error", message: e.message};
|
||||||
|
delete this.warned[t.id];
|
||||||
|
delete this.qrInfo[t.id];
|
||||||
|
delete this.textInfo[t.id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,23 @@ const QR_ONLY_TEMPLATES = [
|
||||||
export const LABEL_TEMPLATES = [
|
export const LABEL_TEMPLATES = [
|
||||||
{
|
{
|
||||||
id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
||||||
required_vars: ["shortId"],
|
required_vars: ["shortId"], tags: ["internal"],
|
||||||
layout: [{type: "mqr", content: c => c.shortId}]
|
layout: [{type: "mqr", content: c => c.shortId}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
||||||
required_vars: ["shortUrl"],
|
required_vars: ["shortUrl"], tags: ["external"],
|
||||||
layout: [{type: "qr-h", content: c => c.shortUrl}]
|
layout: [{type: "qr-h", content: c => c.shortUrl}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
|
||||||
|
required_vars: ["shortUrl","itemId"], tags: ["external"],
|
||||||
|
layout: [{type: "rmqr", content: c => c.shortUrl},GAP,{type: "text", content: c => c.itemId?.toString().padStart(4, "0")}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mqr-tokem-id", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
|
||||||
|
required_vars: ["shortId","itemId"], tags: ["internal"],
|
||||||
|
layout: [{type: "mqr", content: c => c.shortId},GAP,{type: "text", content: c => c.itemId?.toString().padStart(4, "0")}]
|
||||||
},...QR_ONLY_TEMPLATES,
|
},...QR_ONLY_TEMPLATES,
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -113,66 +123,66 @@ export const LABEL_TEMPLATES = [
|
||||||
{
|
{
|
||||||
id: "item-handle", name: "Item handle",
|
id: "item-handle", name: "Item handle",
|
||||||
description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
|
description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
|
||||||
required_vars: ["itemHandle"],
|
required_vars: ["itemHandle"], tags: ["internal"],
|
||||||
layout: [{type: "text", content: c => c.itemHandle}]
|
layout: [{type: "text", content: c => c.itemHandle}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url", name: "Item URL",
|
id: "item-url", name: "Item URL",
|
||||||
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
||||||
required_vars: ["itemUrl"],
|
required_vars: ["itemUrl"], tags: ["external"],
|
||||||
layout: [{type: "text", content: c => c.itemUrl}]
|
layout: [{type: "text", content: c => c.itemUrl}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
|
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
|
||||||
required_vars: ["userHandle"],
|
required_vars: ["userHandle"], tags: ["internal"],
|
||||||
layout: [{type: "text", content: c => c.userHandle}]
|
layout: [{type: "text", content: c => c.userHandle}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.",
|
id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.",
|
||||||
required_vars: ["itemId"],
|
required_vars: ["itemId"], tags: ["internal"],
|
||||||
layout: [{type: "text", content: c => c.itemId}]
|
layout: [{type: "text", content: c => c.itemId}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "owner-id-text", name: "Owner + item ID",
|
id: "owner-id-text", name: "Owner + item ID",
|
||||||
description: "The owner's handle and the item id, as two lines of text - no code.",
|
description: "The owner's handle and the item id, as two lines of text - no code.",
|
||||||
required_vars: ["userHandle", "itemId"],
|
required_vars: ["userHandle", "itemId"], tags: ["internal"],
|
||||||
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
|
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url-qr-handle", name: "Item URL + handle",
|
id: "item-url-qr-handle", name: "Item URL + handle",
|
||||||
description: "Scannable item URL, with the item's compact handle printed alongside.",
|
description: "Scannable item URL, with the item's compact handle printed alongside.",
|
||||||
required_vars: ["itemUrl", "itemHandle"],
|
required_vars: ["itemUrl", "itemHandle"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}]
|
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url-qr-owner", name: "Item URL + owner",
|
id: "item-url-qr-owner", name: "Item URL + owner",
|
||||||
description: "Scannable item URL, with the owner's handle printed alongside.",
|
description: "Scannable item URL, with the owner's handle printed alongside.",
|
||||||
required_vars: ["itemUrl", "userHandle"],
|
required_vars: ["itemUrl", "userHandle"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}]
|
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url-qr-id", name: "Item URL + item ID",
|
id: "item-url-qr-id", name: "Item URL + item ID",
|
||||||
description: "Scannable item URL, with the bare item id printed alongside.",
|
description: "Scannable item URL, with the bare item id printed alongside.",
|
||||||
required_vars: ["itemUrl", "itemId"],
|
required_vars: ["itemUrl", "itemId"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}]
|
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
|
id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
|
||||||
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
||||||
required_vars: ["itemUrl", "userHandle", "itemId"],
|
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}]
|
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "short-url-qr", name: "Short link (QR code)",
|
id: "short-url-qr", name: "Short link (QR code)",
|
||||||
description: "Scannable short link for this item or storage location - more compact than "
|
description: "Scannable short link for this item or storage location - more compact than "
|
||||||
+ "the full URL. Available for any element with a resolvable short link, not just items.",
|
+ "the full URL. Available for any element with a resolvable short link, not just items.",
|
||||||
required_vars: ["shortUrl"],
|
required_vars: ["shortUrl"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.shortUrl}]
|
layout: [{type: "qr", content: c => c.shortUrl}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
|
id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
|
||||||
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
||||||
required_vars: ["itemUrl", "userHandle", "itemId"],
|
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
|
||||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{
|
layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{
|
||||||
type: "text",
|
type: "text",
|
||||||
content: c => c.userHandle
|
content: c => c.userHandle
|
||||||
|
|
@ -180,6 +190,10 @@ export const LABEL_TEMPLATES = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Every distinct tag value used across LABEL_TEMPLATES' tags, in first-seen order - drives
|
||||||
|
// LabelLayoutPreview.vue's filter buttons without hand-listing "internal"/"external" there.
|
||||||
|
export const LABEL_TAGS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.tags ?? []))];
|
||||||
|
|
||||||
// Every field name any template's required_vars names, in first-seen order.
|
// Every field name any template's required_vars names, in first-seen order.
|
||||||
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];
|
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,24 @@ function isQrLeaf(node) {
|
||||||
|
|
||||||
function encodeQr(text, codeType, options) {
|
function encodeQr(text, codeType, options) {
|
||||||
if (!anyd) {
|
if (!anyd) {
|
||||||
throw new Error("The QR encoder is still loading — try again in a moment.");
|
const err = new Error("The QR encoder is still loading — try again in a moment.");
|
||||||
|
err.short = "loading…";
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
// BitMatrix-alike shim over anyd's row-major matrix, matching the old "qrcode" package's
|
// BitMatrix-alike shim over anyd's row-major matrix, matching the old "qrcode" package's
|
||||||
// modules.size/get() shape that drawQrLeaf/snapQrToCrispSize expect. See
|
// modules.size/get() shape that drawQrLeaf/snapQrToCrispSize expect. See
|
||||||
// docs/implementation.md#qr-module-matrix-shim.
|
// docs/implementation.md#qr-module-matrix-shim.
|
||||||
|
try {
|
||||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||||
|
} catch (e) {
|
||||||
|
// anyd's own errors (e.g. "capacity exceeded: …") have no `short` of their own - every
|
||||||
|
// failure here comes down to the content not fitting the chosen QR variant's capacity.
|
||||||
|
if (e && typeof e === "object" && !("short" in e)) {
|
||||||
|
e.short = "too long";
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */
|
const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */
|
||||||
|
|
@ -80,8 +91,26 @@ function fontFamilyFor(fontPx) {
|
||||||
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
|
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tom Thumb reads clearly down to 5px per the same real-Chromium testing as PIXEL_FONT_TIERS; below this, drawTextLeaf blanks the field instead of rejecting the whole label.
|
// Below this font size (px) or physical height (mm) - Tom Thumb's real-Chromium-tested legibility
|
||||||
const MIN_READABLE_TEXT_PX = 5;
|
// floor - text is a hard failure, the same two-metric shape as MIN_RECOMMENDED_QR_PX_PER_MODULE/
|
||||||
|
// MIN_RECOMMENDED_QR_MODULE_MM below (see checkTextSizes). No longer just silently blanked.
|
||||||
|
const MIN_TEXT_PX = 5;
|
||||||
|
const MIN_TEXT_MM = 0.5;
|
||||||
|
// Below this, still legible but a soft warning - flagged rather than blocking, same idea as
|
||||||
|
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||||
|
const MIN_RECOMMENDED_TEXT_PX = 8;
|
||||||
|
const MIN_RECOMMENDED_TEXT_MM = 1;
|
||||||
|
|
||||||
|
// Below this many pixels per module, a QR-family code still technically fits (see
|
||||||
|
// snapQrToCrispSize's scale >= 1 hard requirement) but risks blurring together on a real thermal
|
||||||
|
// printer's dot pitch - a soft warning rather than the outright rejection scale < 1 gets.
|
||||||
|
const MIN_RECOMMENDED_QR_PX_PER_MODULE = 3;
|
||||||
|
// Below this per-module physical size (in mm), a QR-family code is a rule-of-thumb risk for a
|
||||||
|
// phone camera to resolve at normal scanning distance even when crisply printed at full
|
||||||
|
// resolution - also a soft warning, independent of the pixels-per-module check above (a printer
|
||||||
|
// can hit that check's px/module floor at any dpi, but only a high enough dpi keeps modules this
|
||||||
|
// physically small still legible).
|
||||||
|
const MIN_RECOMMENDED_QR_MODULE_MM = 0.5;
|
||||||
|
|
||||||
function isSplit(node) {
|
function isSplit(node) {
|
||||||
return Array.isArray(node);
|
return Array.isArray(node);
|
||||||
|
|
@ -169,27 +198,102 @@ function positionTree(node, ownAxis, x, y) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pins each QR-family leaf's real crisp-pixel box.width/box.height so relation() above starts
|
// Pins each QR-family leaf's real crisp-pixel box.width/box.height so relation() above starts
|
||||||
// treating it as fixed-size. See docs/implementation.md#crisp-qr-sizing.
|
// treating it as fixed-size. Appends a {short, message} entry to `warnings` for each of
|
||||||
function snapQrToCrispSize(node) {
|
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM the leaf falls short of (still
|
||||||
|
// printable, just flagged as a scan-reliability risk), and unconditionally appends its raw
|
||||||
|
// {scale, moduleMm} to `qrInfo` - callers needing to show those numbers regardless of whether
|
||||||
|
// they tripped a threshold (see LabelLayoutPreview.vue's debug line) shouldn't have to re-derive
|
||||||
|
// them. See docs/implementation.md#crisp-qr-sizing.
|
||||||
|
function snapQrToCrispSize(node, pxPerMm, warnings, qrInfo) {
|
||||||
if (isSplit(node)) {
|
if (isSplit(node)) {
|
||||||
node.forEach(snapQrToCrispSize);
|
node.forEach(child => snapQrToCrispSize(child, pxPerMm, warnings, qrInfo));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isQrLeaf(node)) {
|
if (isQrLeaf(node)) {
|
||||||
const {width: modulesW, height: modulesH} = node.qr;
|
const {width: modulesW, height: modulesH} = node.qr;
|
||||||
const scale = Math.floor(Math.min(node.box.width / modulesW, node.box.height / modulesH));
|
const scale = Math.floor(Math.min(node.box.width / modulesW, node.box.height / modulesH));
|
||||||
if (!(scale >= 1)) {
|
if (!(scale >= 1)) {
|
||||||
throw new Error("This text needs a bigger code than the tape allows — "
|
const err = new Error("This text needs a bigger code than the tape allows — "
|
||||||
+ "try a shorter value or a wider tape.");
|
+ "try a shorter value or a wider tape.");
|
||||||
|
err.short = "too big";
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
node.crispWidth = modulesW * scale;
|
node.crispWidth = modulesW * scale;
|
||||||
node.crispHeight = modulesH * scale;
|
node.crispHeight = modulesH * scale;
|
||||||
|
|
||||||
|
const moduleMm = scale / pxPerMm;
|
||||||
|
qrInfo.push({scale, moduleMm});
|
||||||
|
|
||||||
|
if (scale < MIN_RECOMMENDED_QR_PX_PER_MODULE) {
|
||||||
|
warnings.push({
|
||||||
|
short: `${scale}px/mod`,
|
||||||
|
message: `This code's modules are only ${scale}px wide - they may blur together `
|
||||||
|
+ "when printed; consider a bigger label or shorter content.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (moduleMm < MIN_RECOMMENDED_QR_MODULE_MM) {
|
||||||
|
warnings.push({
|
||||||
|
short: `${moduleMm.toFixed(2)}mm/mod`,
|
||||||
|
message: `This code's modules are only ${moduleMm.toFixed(2)}mm across - it may `
|
||||||
|
+ "be too small to scan reliably; consider a bigger label or shorter content.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rendered font size, floored to a whole pixel - same reasoning as snapQrToCrispSize's
|
||||||
|
// integer module scale (rounding up could overflow the box a fractional size would have fit
|
||||||
|
// exactly). checkTextSizes and drawTextLeaf both call this rather than each computing their own
|
||||||
|
// raw fraction, so what gets measured/threshold-checked is exactly what gets drawn.
|
||||||
|
function effectiveFontPx(node, referencePx) {
|
||||||
|
return Math.floor(referencePx * (node.box.height / node.naturalHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same shape as snapQrToCrispSize but for "text" leaves: throws below MIN_TEXT_PX/MIN_TEXT_MM
|
||||||
|
// (too small to read at all), appends a {short, message} warning to `warnings` for each of
|
||||||
|
// MIN_RECOMMENDED_TEXT_PX/MIN_RECOMMENDED_TEXT_MM it falls short of, and unconditionally appends
|
||||||
|
// its raw {fontPx, fontMm} to `textInfo`. Must run after the tree's *final* layoutTree pass (unlike
|
||||||
|
// snapQrToCrispSize, which runs before solve() re-resolves the tree) - a text leaf's box.height
|
||||||
|
// isn't stable until then, since (unlike a QR leaf's crispWidth/crispHeight) it doesn't feed back
|
||||||
|
// into that re-resolve.
|
||||||
|
function checkTextSizes(node, referencePx, pxPerMm, warnings, textInfo) {
|
||||||
|
if (isSplit(node)) {
|
||||||
|
node.forEach(child => checkTextSizes(child, referencePx, pxPerMm, warnings, textInfo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.type !== "text") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fontPx = effectiveFontPx(node, referencePx);
|
||||||
|
const fontMm = fontPx / pxPerMm;
|
||||||
|
if (fontPx < MIN_TEXT_PX || fontMm < MIN_TEXT_MM) {
|
||||||
|
const err = new Error(`This text only fits at ${fontPx}px `
|
||||||
|
+ `(${fontMm.toFixed(2)}mm) - too small to read; try a shorter value, a different `
|
||||||
|
+ "layout, or a bigger label.");
|
||||||
|
err.short = "too small";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
textInfo.push({fontPx, fontMm});
|
||||||
|
|
||||||
|
if (fontPx < MIN_RECOMMENDED_TEXT_PX) {
|
||||||
|
warnings.push({
|
||||||
|
short: `${fontPx}px text`,
|
||||||
|
message: `This text renders at only ${fontPx}px - it may be hard to read; `
|
||||||
|
+ "consider a bigger label or shorter content.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fontMm < MIN_RECOMMENDED_TEXT_MM) {
|
||||||
|
warnings.push({
|
||||||
|
short: `${fontMm.toFixed(2)}mm text`,
|
||||||
|
message: `This text renders at only ${fontMm.toFixed(2)}mm tall - it may be hard to `
|
||||||
|
+ "read; consider a bigger label or shorter content.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always measures in sans-serif at the reference size, since the eventual font (see
|
// Always measures in sans-serif at the reference size, since the eventual font (see
|
||||||
// PIXEL_FONT_TIERS) isn't known until layoutTree sizes the box this aspect ratio feeds into; the
|
// PIXEL_FONT_TIERS) isn't known until layoutTree sizes the box this aspect ratio feeds into; the
|
||||||
// resulting mismatch is invisible in practice, and MIN_READABLE_TEXT_PX still catches real failures.
|
// resulting mismatch is invisible in practice, and checkTextSizes still catches real failures.
|
||||||
function measureTextBlock(ctx, lines, referencePx) {
|
function measureTextBlock(ctx, lines, referencePx) {
|
||||||
ctx.font = `${referencePx}px sans-serif`;
|
ctx.font = `${referencePx}px sans-serif`;
|
||||||
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
|
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
|
||||||
|
|
@ -232,25 +336,21 @@ function drawQrLeaf(ctx, node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the effective (un-normalized) font size drawn at, or that would have been if too small
|
// Returns the effective (un-normalized) font size drawn at; drawTree collects these into
|
||||||
// to draw (see below); drawTree collects these into drawLabel/drawFallbackLabel's textSizesPx.
|
// drawLabel/drawFallbackLabel's textSizesPx. Always draws - checkTextSizes (run earlier, on the
|
||||||
|
// same tree, before any of this) already rejected anything below MIN_TEXT_PX/MIN_TEXT_MM, so
|
||||||
|
// there's no "too small to draw" case left to special-case here.
|
||||||
function drawTextLeaf(ctx, node, referencePx) {
|
function drawTextLeaf(ctx, node, referencePx) {
|
||||||
const fontPx = referencePx * (node.box.height / node.naturalHeight);
|
const fontPx = effectiveFontPx(node, referencePx);
|
||||||
// Below the smallest legible size, leave this leaf blank rather than reject the whole label;
|
|
||||||
// its box was already accounted for, so nothing else in the layout shifts.
|
|
||||||
if (fontPx < MIN_READABLE_TEXT_PX) {
|
|
||||||
return fontPx;
|
|
||||||
}
|
|
||||||
const {family, scale = 1} = fontFamilyFor(fontPx);
|
const {family, scale = 1} = fontFamilyFor(fontPx);
|
||||||
const isPixelFont = family !== "sans-serif";
|
const isPixelFont = family !== "sans-serif";
|
||||||
// Pixel-font glyphs need whole-pixel size/position to stay grid-aligned, since node.box.x/y
|
// Position (not size - fontPx is already a whole pixel) still needs snapping for pixel fonts
|
||||||
// are ordinary (fractional) layout math; sans-serif is left exact since anti-aliasing handles
|
// to stay grid-aligned, since node.box.x/y are ordinary (fractional) layout math; sans-serif is
|
||||||
// fractional positions fine.
|
// left exact since anti-aliasing handles fractional positions fine.
|
||||||
const snap = isPixelFont ? Math.round : (v) => v;
|
const snap = isPixelFont ? Math.round : (v) => v;
|
||||||
const drawFontPx = snap(fontPx);
|
|
||||||
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
|
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
|
||||||
// for its real ink; drawFontPx itself stays the logical size used for centering/stacking math.
|
// for its real ink; fontPx itself stays the logical size used for centering/stacking math.
|
||||||
ctx.font = `${drawFontPx * scale}px "${family}"`;
|
ctx.font = `${fontPx * scale}px "${family}"`;
|
||||||
// Canvas text silently falls back if drawn before a not-yet-loaded font resolves, unlike DOM
|
// Canvas text silently falls back if drawn before a not-yet-loaded font resolves, unlike DOM
|
||||||
// text. See docs/implementation.md#canvas-font-loading.
|
// text. See docs/implementation.md#canvas-font-loading.
|
||||||
if (isPixelFont) {
|
if (isPixelFont) {
|
||||||
|
|
@ -292,8 +392,8 @@ function drawDebugBorder(ctx, node) {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collects each text leaf's effective font size so callers (Print.vue) can spot a blank-rendered
|
// Collects each text leaf's effective font size so callers (Print.vue) can spot a suspiciously
|
||||||
// field (see drawTextLeaf's MIN_READABLE_TEXT_PX check) as suspiciously small rather than silently missing.
|
// small field (checkTextSizes' warnings/textInfo cover the same numbers in more structured form).
|
||||||
function drawTree(ctx, node, referencePx, textSizesPx) {
|
function drawTree(ctx, node, referencePx, textSizesPx) {
|
||||||
if (isSplit(node)) {
|
if (isSplit(node)) {
|
||||||
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
||||||
|
|
@ -311,8 +411,8 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds, sizes and validates the tree for a fixed dimension plus pxPerMm; runs sizing twice so
|
// Builds, sizes and validates the tree for a fixed dimension plus pxPerMm; runs sizing twice so
|
||||||
// QR-family leaves' real crisp size is known before the tree is finally resolved. See
|
// QR-family leaves' real crisp size is known before the tree is finally resolved, then checks
|
||||||
// docs/implementation.md#label-content-layout.
|
// every text leaf's final font size. See docs/implementation.md#label-content-layout.
|
||||||
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
|
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
|
||||||
const tree = buildRenderTree(ctx, content, referencePx);
|
const tree = buildRenderTree(ctx, content, referencePx);
|
||||||
const alongTape = orientation !== "across";
|
const alongTape = orientation !== "across";
|
||||||
|
|
@ -326,29 +426,38 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
|
||||||
|
|
||||||
let {width, height} = solve();
|
let {width, height} = solve();
|
||||||
layoutTree(tree, false, width, height, pxPerMm);
|
layoutTree(tree, false, width, height, pxPerMm);
|
||||||
snapQrToCrispSize(tree);
|
const warnings = [];
|
||||||
|
const qrInfo = [];
|
||||||
|
snapQrToCrispSize(tree, pxPerMm, warnings, qrInfo);
|
||||||
|
|
||||||
({width, height} = solve());
|
({width, height} = solve());
|
||||||
const length = alongTape ? width : height;
|
const length = alongTape ? width : height;
|
||||||
if (maxLength !== Infinity && length > maxLength) {
|
if (maxLength !== Infinity && length > maxLength) {
|
||||||
throw new Error("This doesn't fit on this tape — "
|
const err = new Error("This doesn't fit on this tape — "
|
||||||
+ "try a shorter value, a different layout, or a bigger label.");
|
+ "try a shorter value, a different layout, or a bigger label.");
|
||||||
|
err.short = "too big";
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
layoutTree(tree, false, width, height, pxPerMm);
|
layoutTree(tree, false, width, height, pxPerMm);
|
||||||
return {tree, length};
|
const textInfo = [];
|
||||||
|
checkTextSizes(tree, referencePx, pxPerMm, warnings, textInfo);
|
||||||
|
return {tree, length, warnings, qrInfo, textInfo};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tape-fed layout: draws a fully resolved content tree (see templateContent) at the tape's
|
// The tape-fed layout: draws a fully resolved content tree (see templateContent) at the tape's
|
||||||
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. Returns
|
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. Returns
|
||||||
// {textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
|
// {textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
|
||||||
// top-to-bottom order.
|
// top-to-bottom order; {warnings}: {short, message} scan-reliability entries from
|
||||||
|
// snapQrToCrispSize/checkTextSizes, if any; {qrInfo}: each QR-family leaf's raw
|
||||||
|
// {scale, moduleMm}; {textInfo}: each text leaf's raw {fontPx, fontMm} - both regardless of
|
||||||
|
// whether they tripped a warning.
|
||||||
export function drawLabel(canvas, tape, content, orientation = "along") {
|
export function drawLabel(canvas, tape, content, orientation = "along") {
|
||||||
const maxLength = tape.printLengthPx
|
const maxLength = tape.printLengthPx
|
||||||
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
||||||
: Infinity;
|
: Infinity;
|
||||||
const measureCtx = canvas.getContext("2d");
|
const measureCtx = canvas.getContext("2d");
|
||||||
const pxPerMm = tape.dpi / 25.4;
|
const pxPerMm = tape.dpi / 25.4;
|
||||||
const {tree, length: contentLength} = layoutContent(
|
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||||
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||||
|
|
||||||
const printedLength = tape.printLengthPx || Math.ceil(contentLength + tape.leadPx + TRAILING_PADDING_PX);
|
const printedLength = tape.printLengthPx || Math.ceil(contentLength + tape.leadPx + TRAILING_PADDING_PX);
|
||||||
|
|
@ -376,7 +485,7 @@ export function drawLabel(canvas, tape, content, orientation = "along") {
|
||||||
positionTree(tree, false, origin, 0);
|
positionTree(tree, false, origin, 0);
|
||||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||||
}
|
}
|
||||||
return {textSizesPx};
|
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
|
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
|
||||||
|
|
@ -384,11 +493,11 @@ const FALLBACK_DPI = 203; /* reference resolution for turning "empty" leaves' m
|
||||||
|
|
||||||
// The no-webusb preview/PNG: same layout tree/renderer as drawLabel, scaled from a fixed
|
// The no-webusb preview/PNG: same layout tree/renderer as drawLabel, scaled from a fixed
|
||||||
// reference height instead. See docs/implementation.md#fallback-label-preview. `orientation` and
|
// reference height instead. See docs/implementation.md#fallback-label-preview. `orientation` and
|
||||||
// the {textSizesPx} return, see drawLabel.
|
// the {textSizesPx, warnings, qrInfo, textInfo} return, see drawLabel.
|
||||||
export function drawFallbackLabel(canvas, content, orientation = "along") {
|
export function drawFallbackLabel(canvas, content, orientation = "along") {
|
||||||
const measureCtx = canvas.getContext("2d");
|
const measureCtx = canvas.getContext("2d");
|
||||||
const pxPerMm = FALLBACK_DPI / 25.4;
|
const pxPerMm = FALLBACK_DPI / 25.4;
|
||||||
const {tree, length: contentLength} = layoutContent(
|
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||||
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||||
|
|
||||||
canvas.width = Math.ceil(contentLength);
|
canvas.width = Math.ceil(contentLength);
|
||||||
|
|
@ -411,7 +520,7 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
|
||||||
positionTree(tree, false, 0, 0);
|
positionTree(tree, false, 0, 0);
|
||||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||||
}
|
}
|
||||||
return {textSizesPx};
|
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
|
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
|
||||||
|
|
|
||||||
66
frontend/src/printerManager.js
Normal file
66
frontend/src/printerManager.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Shared printer device manager for WebUSB label printers (Print.vue) - centralizes device
|
||||||
|
// enumeration and preferred-printer memory, mirroring cameraManager.js's approach for webcams.
|
||||||
|
class PrinterManager {
|
||||||
|
constructor() {
|
||||||
|
this.availableDevices = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A USB device has no stable "deviceId" like MediaDeviceInfo - vendorId+productId+serialNumber
|
||||||
|
// is the closest stable identity across plug/unplug and page reloads (empty serial number still
|
||||||
|
// uniquely identifies the common single-printer-of-that-model setup).
|
||||||
|
printerId(device) {
|
||||||
|
return `${device.vendorId}:${device.productId}:${device.serialNumber || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async enumerateDevices() {
|
||||||
|
try {
|
||||||
|
this.availableDevices = await navigator.usb.getDevices();
|
||||||
|
return this.availableDevices;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error enumerating printers:', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableDevices() {
|
||||||
|
return this.availableDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentPrinters() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('recentPrinterIds');
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading recent printers:', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savePreferredPrinter(device) {
|
||||||
|
const printerId = this.printerId(device);
|
||||||
|
const recentPrinters = this.getRecentPrinters();
|
||||||
|
const updated = [printerId, ...recentPrinters.filter((id) => id !== printerId)];
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('recentPrinterIds', JSON.stringify(updated));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving recent printers:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most-recently-used device that's among `devices` (already-paired printers currently
|
||||||
|
// visible), for auto-connecting on page load / when a printer is plugged back in - same
|
||||||
|
// "recent list, first still-present match wins" logic as cameraManager's loadPreferredCamera.
|
||||||
|
findPreferredDevice(devices) {
|
||||||
|
const recentPrinters = this.getRecentPrinters();
|
||||||
|
for (const printerId of recentPrinters) {
|
||||||
|
const device = devices.find((d) => this.printerId(d) === printerId);
|
||||||
|
if (device) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PrinterManager();
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
|
|
||||||
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="warnings.length" class="alert alert-warning" role="alert">
|
||||||
|
<div v-for="(w, i) in warnings" :key="i">{{ w.message }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!usbSupported" class="alert alert-warning">
|
<div v-if="!usbSupported" class="alert alert-warning">
|
||||||
This browser can't talk to USB label printers directly. Open this page in Chrome, Edge or
|
This browser can't talk to USB label printers directly. Open this page in Chrome, Edge or
|
||||||
Opera over https:// (or http://localhost) to print straight from here, or use one of the
|
Opera over https:// (or http://localhost) to print straight from here, or use one of the
|
||||||
|
|
@ -244,7 +248,7 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label-layout-preview :fields="fields" :value="selectedTemplate"
|
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="tape"
|
||||||
:recent-template-ids="recentTemplateIds"
|
:recent-template-ids="recentTemplateIds"
|
||||||
@input="selectedTemplate = $event"></label-layout-preview>
|
@input="selectedTemplate = $event"></label-layout-preview>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -260,6 +264,7 @@ import {markRaw, nextTick} from "vue";
|
||||||
import {mapActions, mapGetters} from "vuex";
|
import {mapActions, mapGetters} from "vuex";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
||||||
|
import printerManager from "@/printerManager.js";
|
||||||
|
|
||||||
import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js";
|
import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js";
|
||||||
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder} from "@/label.js";
|
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder} from "@/label.js";
|
||||||
|
|
@ -319,6 +324,8 @@ export default {
|
||||||
printedWidthPx: 0,
|
printedWidthPx: 0,
|
||||||
// Each "text" leaf's effective font size (see label.js's drawLabel), shown beside the tape width so a too-small-to-render field reads as a suspiciously tiny number rather than silently absent.
|
// Each "text" leaf's effective font size (see label.js's drawLabel), shown beside the tape width so a too-small-to-render field reads as a suspiciously tiny number rather than silently absent.
|
||||||
textSizesPx: [],
|
textSizesPx: [],
|
||||||
|
// Scan-reliability messages from label.js's drawLabel/drawFallbackLabel (too few px per QR module, or too small in mm) - the label still rendered/prints fine, just flagged as a risk.
|
||||||
|
warnings: [],
|
||||||
|
|
||||||
// One input per BASE_VARS entry; derived vars (userHandle/itemUrl/itemHandle) are calculated-only (see the `fields` computed), never stored here. Prefilled from query params but left editable.
|
// One input per BASE_VARS entry; derived vars (userHandle/itemUrl/itemHandle) are calculated-only (see the `fields` computed), never stored here. Prefilled from query params but left editable.
|
||||||
varValues: {
|
varValues: {
|
||||||
|
|
@ -554,7 +561,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshDevices() {
|
async refreshDevices() {
|
||||||
this.devices = (await navigator.usb.getDevices()).map((d) => markRaw(d));
|
this.devices = (await printerManager.enumerateDevices()).map((d) => markRaw(d));
|
||||||
/* A printer unplugged while open drops from the list: its handle is gone, so close the card rather than keep it open on a dead connection. */
|
/* A printer unplugged while open drops from the list: its handle is gone, so close the card rather than keep it open on a dead connection. */
|
||||||
if (this.connected !== null && !this.devices.includes(this.connected)) {
|
if (this.connected !== null && !this.devices.includes(this.connected)) {
|
||||||
this.connected = null;
|
this.connected = null;
|
||||||
|
|
@ -584,18 +591,32 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
connect(index) {
|
|
||||||
this.guard(async () => {
|
|
||||||
// Only one open connection at a time: connecting a different printer disconnects the current one first.
|
// Only one open connection at a time: connecting a different printer disconnects the current one first.
|
||||||
|
async connectToDevice(device) {
|
||||||
await this.closeConnection();
|
await this.closeConnection();
|
||||||
const device = this.devices[index];
|
|
||||||
this.blob.setDevices([device]);
|
this.blob.setDevices([device]);
|
||||||
await this.blob.open(device.vendorId, device.productId);
|
await this.blob.open(device.vendorId, device.productId);
|
||||||
this.connected = device;
|
this.connected = device;
|
||||||
|
printerManager.savePreferredPrinter(device);
|
||||||
const status = await this.blob.status();
|
const status = await this.blob.status();
|
||||||
// Setting `tape` alone is enough to redraw - see the tape watcher above.
|
// Setting `tape` alone is enough to redraw - see the tape watcher above.
|
||||||
this.tape = this.blob.can("print") ? tapeFromStatus(status) : null;
|
this.tape = this.blob.can("print") ? tapeFromStatus(status) : null;
|
||||||
});
|
},
|
||||||
|
|
||||||
|
connect(index) {
|
||||||
|
this.guard(() => this.connectToDevice(this.devices[index]));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Auto-connects to the most-recently-used printer once it's paired and visible again (page
|
||||||
|
// load, or plugged back in - see onUsbChange), mirroring cameraManager's preferred-camera
|
||||||
|
// auto-select. Gated on "nothing connected yet" since a manual connect() to a different
|
||||||
|
// paired printer shouldn't be silently overridden by a later device-list refresh.
|
||||||
|
autoConnectPreferred() {
|
||||||
|
if (this.connected) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const preferred = printerManager.findPreferredDevice(this.devices);
|
||||||
|
return preferred ? this.connectToDevice(preferred) : Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|
@ -613,15 +634,16 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.resizeObserver.observe(canvas.parentElement);
|
this.resizeObserver.observe(canvas.parentElement);
|
||||||
let textSizesPx;
|
let textSizesPx, warnings;
|
||||||
try {
|
try {
|
||||||
({textSizesPx} = drawLabel(canvas, this.tape, content, this.orientation));
|
({textSizesPx, warnings} = drawLabel(canvas, this.tape, content, this.orientation));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e.message;
|
this.error = e.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.textSizesPx = textSizesPx;
|
this.textSizesPx = textSizesPx;
|
||||||
|
this.warnings = warnings;
|
||||||
const bitmap = canvasToBitmap(canvas);
|
const bitmap = canvasToBitmap(canvas);
|
||||||
bitmapToCanvas(canvas, bitmap);
|
bitmapToCanvas(canvas, bitmap);
|
||||||
this.labelBitmap = bitmap;
|
this.labelBitmap = bitmap;
|
||||||
|
|
@ -640,13 +662,15 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.resizeObserver.observe(canvas.parentElement);
|
this.resizeObserver.observe(canvas.parentElement);
|
||||||
|
let warnings;
|
||||||
try {
|
try {
|
||||||
drawFallbackLabel(canvas, content, this.orientation);
|
({warnings} = drawFallbackLabel(canvas, content, this.orientation));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e.message;
|
this.error = e.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
this.warnings = warnings;
|
||||||
this.fallbackReady = true;
|
this.fallbackReady = true;
|
||||||
this.fitZoom(canvas);
|
this.fitZoom(canvas);
|
||||||
},
|
},
|
||||||
|
|
@ -700,7 +724,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onUsbChange() {
|
onUsbChange() {
|
||||||
this.guard(() => this.refreshDevices());
|
this.guard(async () => {
|
||||||
|
await this.refreshDevices();
|
||||||
|
await this.autoConnectPreferred();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Works around Vue's refInFor array-collecting behavior for :ref in v-for, same as LabelLayoutPreview.vue's setTemplateCanvasRef.
|
// Works around Vue's refInFor array-collecting behavior for :ref in v-for, same as LabelLayoutPreview.vue's setTemplateCanvasRef.
|
||||||
|
|
@ -779,7 +806,10 @@ export default {
|
||||||
navigator.usb.addEventListener("connect", this.onUsbChange);
|
navigator.usb.addEventListener("connect", this.onUsbChange);
|
||||||
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
||||||
await qrReady;
|
await qrReady;
|
||||||
await this.guard(() => this.refreshDevices());
|
await this.guard(async () => {
|
||||||
|
await this.refreshDevices();
|
||||||
|
await this.autoConnectPreferred();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if ("usb" in navigator) {
|
if ("usb" in navigator) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@
|
||||||
<div class="invalid-feedback">{{ errors.email }}</div>
|
<div class="invalid-feedback">{{ errors.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="text" class="visually-hidden" autocomplete="username"
|
||||||
|
tabindex="-1" aria-hidden="true"
|
||||||
|
v-model="fullHandle"/>
|
||||||
|
|
||||||
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<input class="form-control form-control-lg" type="password"
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
|
@ -117,8 +121,39 @@ export default {
|
||||||
domains: []
|
domains: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
fullHandle: {
|
||||||
|
get() {
|
||||||
|
return this.form.domain ? `${this.form.username}@${this.form.domain}` : this.form.username;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.applyHandle(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// Catches the same "user@domain" shape when it's typed or pasted
|
||||||
|
// directly into the visible username field instead.
|
||||||
|
'form.username'(value) {
|
||||||
|
if (value.includes('@')) {
|
||||||
|
this.applyHandle(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['lookupServer']),
|
...mapActions(['lookupServer']),
|
||||||
|
applyHandle(value) {
|
||||||
|
const atIndex = value.indexOf('@');
|
||||||
|
if (atIndex === -1) {
|
||||||
|
this.form.username = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.username = value.slice(0, atIndex);
|
||||||
|
const domain = value.slice(atIndex + 1);
|
||||||
|
if (this.domains.includes(domain)) {
|
||||||
|
this.form.domain = domain;
|
||||||
|
}
|
||||||
|
},
|
||||||
do_register() {
|
do_register() {
|
||||||
console.log('do_register');
|
console.log('do_register');
|
||||||
console.log(this.form);
|
console.log(this.form);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue