Compare commits

..
Author SHA1 Message Date
Maarten Lakerveld b9985c56b5 Version the sprite URL with a hash of the vendored sheet
Core serves /static with a month of max-age and the service worker does
not precache the map assets, so a re-vendored sheet at the same URL would
keep coming from the browser cache next to a freshly fetched style that
expects the new icons. Append a short content hash of the four sheet
files as a query parameter. MapLibre appends the format and extension to
the pathname, so the query survives, and the test now guards that the
absolute-URL rewrite keeps it too.
2026-09-28 16:31:38 +02:00
Maarten Lakerveld 49e7e3a7ec Only treat image properties as sprite references
The sprite check walked every string under layout and paint, so any
base-prefixed text value would have failed the required lint job without
asking for an image. Collect from the image-bearing properties only.
2026-09-28 16:31:29 +02:00
Maarten Lakerveld 358cc8a3fa Gate merges on the bundled map sprite sheet
A @versatiles/style bump that references new icons used to fail only the
build job, which the dev ruleset does not require, so the bump could be
merged red and the post-merge sync workflow was left to repair it. Run
the sprite check in the required lint job instead, so the sheet has to be
re-vendored on the dependency PR itself, and drop the sync workflow that
existed to clean up after the merge.
2026-09-28 16:01:54 +02:00
Maarten Lakerveld 6dcdf37fbc Note that the sprite sheet ships with the frontend
The module header still said sprites come through core's proxy, which
stopped being true when the sheet was bundled.
2026-09-28 15:54:03 +02:00
Maarten Lakerveld 797b8564ba Format map-assets.js with Prettier 2026-09-28 15:51:44 +02:00
Maarten Lakerveld fcc48aa312 Exclude vendored map sprite metadata from Prettier
The sprite JSON is extracted verbatim from the @versatiles/style release
in its upstream compact form, which fails prettier --check. Ignoring it
keeps lint green and keeps the sync workflow's drift check byte-identical
with upstream.
2026-09-28 15:43:01 +02:00
Maarten Lakerveld 991c146d90 Open a PR when the bundled map sprite sheet drifts
Runs the sprite update after a push to dev touches package.json, which
is how a @versatiles/style bump lands, and opens a PR with the
re-vendored sheet when it changed. Mirrors the device class and sensor
constant sync workflows.
2026-09-28 15:43:01 +02:00
Maarten Lakerveld 47633c3bd7 Bundle the VersaTiles base sprite sheet
The styles referenced the sprite sheet through core's proxy, which
fetched it from the OpenStreetMap Foundation. The OSMF only mirrors
the retired v5 basics sheet, and VersaTiles warns that the assets on
tiles.versatiles.org change with every release, so neither is a stable
source for the exact sheet the pinned @versatiles/style expects.

The base sheet now ships with the frontend under static/map/sprites,
fetched from the versatiles-style release matching the installed
package with `yarn gulp update-map-sprites`. The style build checks
that every icon the styles reference exists in the bundled sheet, so a
bump that renames icons fails the build instead of shipping POIs
without icons. Sprites resolve against the page rather than the
instance, since on Cast the frontend is not served by the instance,
and the demo no longer needs its own sprite override.
2026-09-28 15:43:01 +02:00
Maarten Lakerveld dafb7f46af Generate the map styles with @versatiles/style v6
v6 removed the colorful and eclipse builders; osm({ theme }) replaces
them, with eclipse mapping to the retuned colorful-dark palette. The
sprite sheet is renamed from basics to base and its icon ids change,
so core's tile proxy has to serve the base sheet before this ships.

v6 also defaults to the globe projection and the styles keep it. Only
the MapLibre engine renders them: from ha-map the Leaflet engine runs
without WebGL2 or raster-only after a MapLibre failure, so nothing
ties the map to Mercator any more.
2026-09-28 15:43:01 +02:00
21 changed files with 720 additions and 58 deletions
+3
View File
@@ -76,6 +76,9 @@ jobs:
- name: Check dependency licenses
if: ${{ !cancelled() && steps.build_resources.outcome == 'success' }}
run: yarn run lint:licenses
- name: Check the bundled map sprite sheet
if: ${{ !cancelled() }}
run: ./node_modules/.bin/gulp check-map-sprites
test:
name: Run tests
needs: prepare-dependencies
+2 -1
View File
@@ -1,4 +1,5 @@
CLA.md
CODE_OF_CONDUCT.md
LICENSE.md
PULL_REQUEST_TEMPLATE.md
PULL_REQUEST_TEMPLATE.md
public/static/map/sprites/*.json
+1
View File
@@ -15,6 +15,7 @@ import "./gen-sensor-entity-constants.js";
import "./landing-page.js";
import "./locale-data.js";
import "./map-assets.js";
import "./map-sprites.js";
import "./rspack.js";
import "./service-worker.js";
import "./translations.js";
+75 -21
View File
@@ -1,17 +1,25 @@
// Generates the MapLibre styles for the vector base map.
//
// Only the styles. Glyphs, sprites and tiles are served by core's proxy, which
// is what lets them be requested with an application User-Agent and without a
// referrer. The styles stay here because they come from @versatiles/style and
// core has no node toolchain to regenerate them with.
// Only the styles. Glyphs and tiles are served by core's proxy, which is what
// lets them be requested with an application User-Agent and without a referrer.
// The sprite sheet ships with the frontend (see map-sprites.js). The styles
// stay here because they come from @versatiles/style and core has no node
// toolchain to regenerate them with.
import { writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { colorful, eclipse } from "@versatiles/style";
import { osm } from "@versatiles/style";
import fs from "fs-extra";
import gulp from "gulp";
import paths from "../paths.cjs";
import { addLatinLabels } from "./map-labels.js";
import {
missingSprites,
SHEET_FILES,
SPRITE_SHEET,
spritesDir,
} from "./map-sprites.js";
const PROXY_PATH = "/api/map_tiles";
const TILEJSON_URL = `${PROXY_PATH}/tilejson.json`;
@@ -50,26 +58,68 @@ const useTileJson = (name, style) => {
return style;
};
const styleOptions = {
// Keeps the generated URLs origin relative.
baseUrl: "",
glyphs: `${PROXY_PATH}/fonts/{fontstack}/{range}.pbf`,
sprite: [{ id: "basics", url: `${PROXY_PATH}/sprites/basics/sprites` }],
// Core serves /static with a month of max-age, so a re-vendored sheet would
// otherwise keep being read from cache next to a style that expects the new one.
const sheetHash = async () => {
const contents = await Promise.all(
SHEET_FILES.map((file) => readFile(path.join(spritesDir, file)))
);
const hash = createHash("sha256");
contents.forEach((content) => hash.update(content));
return hash.digest("hex").slice(0, 8);
};
const styleOptions = (spriteVersion) => ({
urls: {
// Keeps the generated URLs origin relative.
base: "",
glyphsPattern: `${PROXY_PATH}/fonts/{fontstack}/{range}.pbf`,
sprite: [
{
id: SPRITE_SHEET,
url: `/static/map/sprites/${SPRITE_SHEET}?v=${spriteVersion}`,
},
],
},
});
const checkSprites = (name, style, sheet) => {
const missing = missingSprites(style, sheet);
if (missing.length) {
throw new Error(
`Style "${name}" references icons missing from the bundled ${SPRITE_SHEET} ` +
`sprite sheet: ${missing.join(", ")}. Run \`yarn gulp update-map-sprites\` ` +
`and commit the result.`
);
}
return style;
};
// Both themes up front: dark is a real cartography, not an inverted raster.
const THEMES = [
["light", "colorful"],
["dark", "colorful-dark"],
];
const generateStyles = async () => {
const sheet = await fs.readJson(
path.join(spritesDir, `${SPRITE_SHEET}.json`)
);
const options = styleOptions(await sheetHash());
return THEMES.map(([name, theme]) => [
name,
addLatinLabels(
checkSprites(name, useTileJson(name, osm({ theme, ...options })), sheet)
),
]);
};
const buildMapAssets = async () => {
await fs.emptyDir(outputDir);
const styles = await generateStyles();
await Promise.all(
// Both themes up front: dark is a real cartography, not an inverted raster.
[
["light", colorful],
["dark", eclipse],
].map(([name, builder]) =>
writeFile(
path.join(outputDir, `${name}.json`),
JSON.stringify(addLatinLabels(useTileJson(name, builder(styleOptions))))
)
styles.map(([name, style]) =>
writeFile(path.join(outputDir, `${name}.json`), JSON.stringify(style))
)
);
};
@@ -83,4 +133,8 @@ export const ensureMapAssets = () => {
gulp.task("build-map-assets", ensureMapAssets);
// Runs in the required lint job so a @versatiles/style bump that needs new
// icons cannot merge before the sheet is re-vendored on that branch.
gulp.task("check-map-sprites", generateStyles);
export const mapAssetsDir = outputDir;
+80
View File
@@ -0,0 +1,80 @@
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import gulp from "gulp";
import paths from "../paths.cjs";
const require = createRequire(import.meta.url);
export const SPRITE_SHEET = "base";
export const spritesDir = path.resolve(
paths.root_dir,
"public",
"static",
"map",
"sprites"
);
export const SHEET_FILES = [".json", ".png", "@2x.json", "@2x.png"].map(
(suffix) => `${SPRITE_SHEET}${suffix}`
);
// The only style properties that resolve to a sprite image.
const IMAGE_PROPERTIES = [
"icon-image",
"fill-pattern",
"line-pattern",
"fill-extrusion-pattern",
"background-pattern",
];
const collectStrings = (value, into) => {
if (typeof value === "string") {
into.push(value);
} else if (Array.isArray(value)) {
value.forEach((item) => collectStrings(item, into));
} else if (value && typeof value === "object") {
Object.values(value).forEach((item) => collectStrings(item, into));
}
return into;
};
export const spriteIds = (style) => {
const prefix = `${SPRITE_SHEET}:`;
const strings = style.layers.flatMap((layer) =>
IMAGE_PROPERTIES.flatMap((property) =>
collectStrings([layer.layout?.[property], layer.paint?.[property]], [])
)
);
return new Set(
strings
.filter((value) => value.startsWith(prefix))
.map((value) => value.slice(prefix.length))
);
};
export const missingSprites = (style, sheet) =>
[...spriteIds(style)].filter((id) => !(id in sheet));
gulp.task("update-map-sprites", async () => {
const { version } = require("@versatiles/style/package.json");
const url = `https://github.com/versatiles-org/versatiles-style/releases/download/v${version}/sprites.tar.gz`;
const response = await fetch(url);
if (!response.ok || !response.body) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
const tar = spawn("tar", ["-xzf", "-", "-C", spritesDir, ...SHEET_FILES], {
stdio: ["pipe", "inherit", "inherit"],
});
const exited = new Promise((resolve, reject) => {
tar.on("error", reject);
tar.on("close", (code) =>
code === 0 ? resolve() : reject(new Error(`tar exited with ${code}`))
);
});
await pipeline(Readable.fromWeb(response.body), tar.stdin);
await exited;
});
+1 -1
View File
@@ -169,7 +169,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@typescript/native": "npm:[email protected]",
"@versatiles/style": "5.13.1",
"@versatiles/style": "6.0.1",
"@vitest/coverage-v8": "5.0.2",
"babel-loader": "10.1.1",
"babel-plugin-polyfill-corejs3": "1.0.0",
+128
View File
@@ -0,0 +1,128 @@
{
"icon-airfield": {"width":32,"height":32,"x":0,"y":0,"pixelRatio":1,"sdf":true,"title":"Airfield","aliases":["aerodrome","airstrip","landing strip"]},
"icon-airport": {"width":32,"height":32,"x":32,"y":0,"pixelRatio":1,"sdf":true,"title":"Airport","aliases":["aeroplane","airplane","plane","flight","terminal"]},
"icon-alcohol_shop": {"width":32,"height":32,"x":0,"y":32,"pixelRatio":1,"sdf":true,"title":"Alcohol shop","aliases":["off-licence","liquor store","bottle shop","wine","spirits"]},
"icon-amusement_park": {"width":32,"height":32,"x":32,"y":32,"pixelRatio":1,"sdf":true,"title":"Amusement park","aliases":["theme park","fairground","rides"]},
"icon-art_gallery": {"width":32,"height":32,"x":64,"y":0,"pixelRatio":1,"sdf":true,"title":"Art gallery","aliases":["picture","painting","exhibition"]},
"icon-artwork": {"width":32,"height":32,"x":64,"y":32,"pixelRatio":1,"sdf":true,"title":"Artwork","aliases":["public art","sculpture","installation"]},
"icon-atm": {"width":32,"height":32,"x":0,"y":64,"pixelRatio":1,"sdf":true,"title":"ATM","aliases":["cash machine","cashpoint","bank machine"]},
"icon-bakery": {"width":32,"height":32,"x":32,"y":64,"pixelRatio":1,"sdf":true,"title":"Bakery","aliases":["bread","baker","patisserie"]},
"icon-bank": {"width":32,"height":32,"x":64,"y":64,"pixelRatio":1,"sdf":true,"title":"Bank","aliases":["branch","banking","finance"]},
"icon-bar": {"width":32,"height":32,"x":96,"y":0,"pixelRatio":1,"sdf":true,"title":"Bar","aliases":["cocktail","drinks","lounge"]},
"icon-beauty": {"width":32,"height":32,"x":96,"y":32,"pixelRatio":1,"sdf":true,"title":"Beauty salon","aliases":["cosmetics","nails","spa"]},
"icon-beer_mug": {"width":32,"height":32,"x":96,"y":64,"pixelRatio":1,"sdf":true,"title":"Beer mug","aliases":["beer","beergarden","beer garden","brewery"]},
"icon-bench": {"width":32,"height":32,"x":0,"y":96,"pixelRatio":1,"sdf":true,"title":"Bench","aliases":["seat","seating","rest"]},
"icon-beverages": {"width":32,"height":32,"x":32,"y":96,"pixelRatio":1,"sdf":true,"title":"Drinks shop","aliases":["soft drinks","juice","off-licence"]},
"icon-bicycle_share": {"width":32,"height":32,"x":64,"y":96,"pixelRatio":1,"sdf":true,"title":"Bike share","aliases":["bicycle sharing","hire","docking station"]},
"icon-books": {"width":32,"height":32,"x":96,"y":96,"pixelRatio":1,"sdf":true,"title":"Bookshop","aliases":["bookstore","reading","literature"]},
"icon-bus": {"width":32,"height":32,"x":128,"y":0,"pixelRatio":1,"sdf":true,"title":"Bus","aliases":["coach","omnibus","public transport"]},
"icon-butcher": {"width":32,"height":32,"x":128,"y":32,"pixelRatio":1,"sdf":true,"title":"Butcher","aliases":["meat","deli","carvery"]},
"icon-cafe": {"width":32,"height":32,"x":128,"y":64,"pixelRatio":1,"sdf":true,"title":"Café","aliases":["coffee","coffee shop","espresso","tea"]},
"icon-campsite": {"width":32,"height":32,"x":128,"y":96,"pixelRatio":1,"sdf":true,"title":"Campsite","aliases":["camping","tent","pitch"]},
"icon-car_rental": {"width":32,"height":32,"x":0,"y":128,"pixelRatio":1,"sdf":true,"title":"Car rental","aliases":["car hire","rent a car"]},
"icon-car_wash": {"width":32,"height":32,"x":32,"y":128,"pixelRatio":1,"sdf":true,"title":"Car wash","aliases":["valeting","vehicle wash"]},
"icon-caravan": {"width":32,"height":32,"x":64,"y":128,"pixelRatio":1,"sdf":true,"title":"Caravan site","aliases":["motorhome","rv","campervan"]},
"icon-castle": {"width":32,"height":32,"x":96,"y":128,"pixelRatio":1,"sdf":true,"title":"Castle","aliases":["fort","fortress","palace","chateau"]},
"icon-cemetery": {"width":32,"height":32,"x":128,"y":128,"pixelRatio":1,"sdf":true,"title":"Cemetery","aliases":["graveyard","burial","grave"]},
"icon-chalet": {"width":32,"height":32,"x":160,"y":0,"pixelRatio":1,"sdf":true,"title":"Chalet","aliases":["cabin","lodge","holiday home"]},
"icon-cinema": {"width":32,"height":32,"x":160,"y":32,"pixelRatio":1,"sdf":true,"title":"Cinema","aliases":["movie theater","movies","film"]},
"icon-clothes": {"width":32,"height":32,"x":160,"y":64,"pixelRatio":1,"sdf":true,"title":"Clothes shop","aliases":["clothing","fashion","apparel","boutique"]},
"icon-college": {"width":32,"height":32,"x":160,"y":96,"pixelRatio":1,"sdf":true,"title":"College","aliases":["university","campus","further education"]},
"icon-community": {"width":32,"height":32,"x":160,"y":128,"pixelRatio":1,"sdf":true,"title":"Community centre","aliases":["community center","village hall","meeting"]},
"icon-defibrillator": {"width":32,"height":32,"x":0,"y":160,"pixelRatio":1,"sdf":true,"title":"Defibrillator","aliases":["aed","heart","resuscitation","emergency"]},
"icon-dentist": {"width":32,"height":32,"x":32,"y":160,"pixelRatio":1,"sdf":true,"title":"Dentist","aliases":["dental","teeth","orthodontist"]},
"icon-dharma_wheel": {"width":32,"height":32,"x":64,"y":160,"pixelRatio":1,"sdf":true,"title":"Dharma wheel","aliases":["buddhist","buddhism","temple","religion"]},
"icon-do_it_yourself": {"width":32,"height":32,"x":96,"y":160,"pixelRatio":1,"sdf":true,"title":"DIY store","aliases":["doityourself","home improvement","builders merchant"]},
"icon-doctor": {"width":32,"height":32,"x":128,"y":160,"pixelRatio":1,"sdf":true,"title":"Doctor","aliases":["gp","surgery","clinic","physician"]},
"icon-dog": {"width":32,"height":32,"x":160,"y":160,"pixelRatio":1,"sdf":true,"title":"Dog park","aliases":["dog_park","dogs","pet","off-leash"]},
"icon-drinking_water": {"width":32,"height":32,"x":192,"y":0,"pixelRatio":1,"sdf":true,"title":"Drinking water","aliases":["tap","potable","refill","water point"]},
"icon-dry_cleaning": {"width":32,"height":32,"x":192,"y":32,"pixelRatio":1,"sdf":true,"title":"Dry cleaning","aliases":["drycleaning","cleaner","launderer"]},
"icon-embassy": {"width":32,"height":32,"x":192,"y":64,"pixelRatio":1,"sdf":true,"title":"Embassy","aliases":["consulate","diplomatic","mission"]},
"icon-emergency_access": {"width":32,"height":32,"x":192,"y":96,"pixelRatio":1,"sdf":true,"title":"Emergency access","aliases":["access point","rescue point"]},
"icon-emergency_phone": {"width":32,"height":32,"x":192,"y":128,"pixelRatio":1,"sdf":true,"title":"Emergency phone","aliases":["sos","help point","call box"]},
"icon-fast_food": {"width":32,"height":32,"x":192,"y":160,"pixelRatio":1,"sdf":true,"title":"Fast food","aliases":["takeaway","burger","snack"]},
"icon-fire_station": {"width":32,"height":32,"x":0,"y":192,"pixelRatio":1,"sdf":true,"title":"Fire station","aliases":["fire brigade","fire service","firefighters"]},
"icon-florist": {"width":32,"height":32,"x":32,"y":192,"pixelRatio":1,"sdf":true,"title":"Florist","aliases":["flowers","flower shop","bouquet"]},
"icon-fountain": {"width":32,"height":32,"x":64,"y":192,"pixelRatio":1,"sdf":true,"title":"Fountain","aliases":["water feature","jet"]},
"icon-fuel_pump": {"width":32,"height":32,"x":96,"y":192,"pixelRatio":1,"sdf":true,"title":"Petrol station","aliases":["fuel","gas station","filling station","diesel"]},
"icon-furniture": {"width":32,"height":32,"x":128,"y":192,"pixelRatio":1,"sdf":true,"title":"Furniture shop","aliases":["furnishings","home store"]},
"icon-garden_center": {"width":32,"height":32,"x":160,"y":192,"pixelRatio":1,"sdf":true,"title":"Garden centre","aliases":["garden_centre","nursery","plants"]},
"icon-gift": {"width":32,"height":32,"x":192,"y":192,"pixelRatio":1,"sdf":true,"title":"Gift shop","aliases":["presents","souvenirs"]},
"icon-golf": {"width":32,"height":32,"x":224,"y":0,"pixelRatio":1,"sdf":true,"title":"Golf","aliases":["golf course","driving range"]},
"icon-greengrocer": {"width":32,"height":32,"x":224,"y":32,"pixelRatio":1,"sdf":true,"title":"Greengrocer","aliases":["fruit","vegetables","produce"]},
"icon-hardware": {"width":32,"height":32,"x":224,"y":64,"pixelRatio":1,"sdf":true,"title":"Hardware shop","aliases":["tools","ironmonger"]},
"icon-historic": {"width":32,"height":32,"x":224,"y":96,"pixelRatio":1,"sdf":true,"title":"Historic site","aliases":["heritage","ruins","antiquity"]},
"icon-hospital": {"width":32,"height":32,"x":224,"y":128,"pixelRatio":1,"sdf":true,"title":"Hospital","aliases":["a&e","emergency room","infirmary","medical"]},
"icon-hunting_stand": {"width":32,"height":32,"x":224,"y":160,"pixelRatio":1,"sdf":true,"title":"Hunting stand","aliases":["huntingstand","hide","deer stand"]},
"icon-hydrant": {"width":32,"height":32,"x":224,"y":192,"pixelRatio":1,"sdf":true,"title":"Fire hydrant","aliases":["fire plug","water point","firefighting"]},
"icon-ice_rink": {"width":32,"height":32,"x":0,"y":224,"pixelRatio":1,"sdf":true,"title":"Ice rink","aliases":["icerink","skating","ice skating"]},
"icon-justice": {"width":32,"height":32,"x":32,"y":224,"pixelRatio":1,"sdf":true,"title":"Courthouse","aliases":["court","law court","tribunal","judiciary"]},
"icon-khanda": {"width":32,"height":32,"x":64,"y":224,"pixelRatio":1,"sdf":true,"title":"Khanda","aliases":["sikh","sikhism","gurdwara","religion"]},
"icon-latin_cross": {"width":32,"height":32,"x":96,"y":224,"pixelRatio":1,"sdf":true,"title":"Latin cross","aliases":["christian","christianity","church","religion"]},
"icon-laundry": {"width":32,"height":32,"x":128,"y":224,"pixelRatio":1,"sdf":true,"title":"Laundry","aliases":["launderette","laundromat","washing"]},
"icon-library": {"width":32,"height":32,"x":160,"y":224,"pixelRatio":1,"sdf":true,"title":"Library","aliases":["lending","public library","archive"]},
"icon-lighthouse": {"width":32,"height":32,"x":192,"y":224,"pixelRatio":1,"sdf":true,"title":"Lighthouse","aliases":["beacon","navigation","coast"]},
"icon-lodging": {"width":32,"height":32,"x":224,"y":224,"pixelRatio":1,"sdf":true,"title":"Hotel","aliases":["accommodation","guest house","hostel","motel","b&b"]},
"icon-marketplace": {"width":32,"height":32,"x":256,"y":0,"pixelRatio":1,"sdf":true,"title":"Marketplace","aliases":["market","stalls","bazaar"]},
"icon-monument": {"width":32,"height":32,"x":256,"y":32,"pixelRatio":1,"sdf":true,"title":"Monument","aliases":["memorial","statue","landmark"]},
"icon-newsagent": {"width":32,"height":32,"x":256,"y":64,"pixelRatio":1,"sdf":true,"title":"Newsagent","aliases":["news","tobacconist","corner shop"]},
"icon-newspaper": {"width":32,"height":32,"x":256,"y":96,"pixelRatio":1,"sdf":true,"title":"Newspaper kiosk","aliases":["kiosk","newsstand","press"]},
"icon-nightclub": {"width":32,"height":32,"x":256,"y":128,"pixelRatio":1,"sdf":true,"title":"Nightclub","aliases":["club","disco","dancing"]},
"icon-nursing_home": {"width":32,"height":32,"x":256,"y":160,"pixelRatio":1,"sdf":true,"title":"Nursing home","aliases":["nursinghome","care home","elderly"]},
"icon-observation_tower": {"width":32,"height":32,"x":256,"y":192,"pixelRatio":1,"sdf":true,"title":"Observation tower","aliases":["lookout","viewing tower","watchtower"]},
"icon-om": {"width":32,"height":32,"x":256,"y":224,"pixelRatio":1,"sdf":true,"title":"Om","aliases":["hindu","hinduism","temple","religion"]},
"icon-optician": {"width":32,"height":32,"x":0,"y":256,"pixelRatio":1,"sdf":true,"title":"Optician","aliases":["glasses","eyewear","optometrist"]},
"icon-outdoor": {"width":32,"height":32,"x":32,"y":256,"pixelRatio":1,"sdf":true,"title":"Outdoor shop","aliases":["camping shop","hiking","mountaineering"]},
"icon-person_kneeling_and_praying": {"width":32,"height":32,"x":64,"y":256,"pixelRatio":1,"sdf":true,"title":"Place of worship","aliases":["place_of_worship","prayer","worship","religion"]},
"icon-picnic_site": {"width":32,"height":32,"x":96,"y":256,"pixelRatio":1,"sdf":true,"title":"Picnic site","aliases":["picnic","picnic table","rest area"]},
"icon-pill": {"width":32,"height":32,"x":128,"y":256,"pixelRatio":1,"sdf":true,"title":"Pharmacy","aliases":["chemist","drugstore","medicine","prescription"]},
"icon-pint_glass": {"width":32,"height":32,"x":160,"y":256,"pixelRatio":1,"sdf":true,"title":"Pub","aliases":["public house","tavern","inn","beer"]},
"icon-pitch": {"width":32,"height":32,"x":192,"y":256,"pixelRatio":1,"sdf":true,"title":"Sports pitch","aliases":["field","court","playing field"]},
"icon-police_officer": {"width":32,"height":32,"x":224,"y":256,"pixelRatio":1,"sdf":true,"title":"Police","aliases":["police station","law enforcement","constabulary"]},
"icon-post": {"width":32,"height":32,"x":256,"y":256,"pixelRatio":1,"sdf":true,"title":"Post office","aliases":["mail","postal","parcel"]},
"icon-postbox": {"width":32,"height":32,"x":288,"y":0,"pixelRatio":1,"sdf":true,"title":"Post box","aliases":["mailbox","letter box","pillar box"]},
"icon-prison": {"width":32,"height":32,"x":288,"y":32,"pixelRatio":1,"sdf":true,"title":"Prison","aliases":["jail","correctional","penitentiary"]},
"icon-rail": {"width":32,"height":32,"x":288,"y":64,"pixelRatio":1,"sdf":true,"title":"Railway","aliases":["train","railway station","metro","subway"]},
"icon-recycling": {"width":32,"height":32,"x":288,"y":96,"pixelRatio":1,"sdf":true,"title":"Recycling","aliases":["bottle bank","waste","recycle centre"]},
"icon-restaurant": {"width":32,"height":32,"x":288,"y":128,"pixelRatio":1,"sdf":true,"title":"Restaurant","aliases":["dining","eat","food","bistro"]},
"icon-restrooms": {"width":32,"height":32,"x":288,"y":160,"pixelRatio":1,"sdf":true,"title":"Toilets","aliases":["toilet","wc","bathroom","lavatory"]},
"icon-ring": {"width":32,"height":32,"x":288,"y":192,"pixelRatio":1,"sdf":true,"title":"Jewellery shop","aliases":["jewelry_store","jeweller","jewelry","rings"]},
"icon-rocking_horse": {"width":32,"height":32,"x":288,"y":224,"pixelRatio":1,"sdf":true,"title":"Toy shop","aliases":["toys","toy store","games"]},
"icon-school": {"width":32,"height":32,"x":288,"y":256,"pixelRatio":1,"sdf":true,"title":"School","aliases":["primary","secondary","education"]},
"icon-scissors_and_comb": {"width":32,"height":32,"x":0,"y":288,"pixelRatio":1,"sdf":true,"title":"Hairdresser","aliases":["barber","salon","haircut","hair"]},
"icon-seesaw": {"width":32,"height":32,"x":32,"y":288,"pixelRatio":1,"sdf":true,"title":"Playground","aliases":["play area","swings","children"]},
"icon-shelter": {"width":32,"height":32,"x":64,"y":288,"pixelRatio":1,"sdf":true,"title":"Shelter","aliases":["bus shelter","refuge","hut"]},
"icon-shoes": {"width":32,"height":32,"x":96,"y":288,"pixelRatio":1,"sdf":true,"title":"Shoe shop","aliases":["footwear","cobbler","trainers"]},
"icon-shop": {"width":32,"height":32,"x":128,"y":288,"pixelRatio":1,"sdf":true,"title":"Shop","aliases":["store","retail","convenience"]},
"icon-shrine": {"width":32,"height":32,"x":160,"y":288,"pixelRatio":1,"sdf":true,"title":"Shrine","aliases":["wayside shrine","sacred","religion"]},
"icon-sports": {"width":32,"height":32,"x":192,"y":288,"pixelRatio":1,"sdf":true,"title":"Sports centre","aliases":["leisure centre","gym","fitness"]},
"icon-stadium": {"width":32,"height":32,"x":224,"y":288,"pixelRatio":1,"sdf":true,"title":"Stadium","aliases":["arena","ground","sports ground"]},
"icon-star_and_crescent": {"width":32,"height":32,"x":256,"y":288,"pixelRatio":1,"sdf":true,"title":"Star and crescent","aliases":["muslim","islam","mosque","religion"]},
"icon-star_of_david": {"width":32,"height":32,"x":288,"y":288,"pixelRatio":1,"sdf":true,"title":"Star of David","aliases":["jewish","judaism","synagogue","religion"]},
"icon-stationery": {"width":32,"height":32,"x":320,"y":0,"pixelRatio":1,"sdf":true,"title":"Stationery shop","aliases":["office supplies","pens","paper"]},
"icon-surveillance": {"width":32,"height":32,"x":320,"y":32,"pixelRatio":1,"sdf":true,"title":"Surveillance camera","aliases":["cctv","camera","security"]},
"icon-swimming": {"width":32,"height":32,"x":320,"y":64,"pixelRatio":1,"sdf":true,"title":"Swimming","aliases":["pool","swimming pool","lido","baths"]},
"icon-telephone": {"width":32,"height":32,"x":320,"y":96,"pixelRatio":1,"sdf":true,"title":"Telephone","aliases":["phone","call box","payphone"]},
"icon-theater": {"width":32,"height":32,"x":320,"y":128,"pixelRatio":1,"sdf":true,"title":"Theatre","aliases":["playhouse","drama","stage","performing arts"]},
"icon-town_hall": {"width":32,"height":32,"x":320,"y":160,"pixelRatio":1,"sdf":true,"title":"Town hall","aliases":["city hall","civic","municipal"]},
"icon-travel_agent": {"width":32,"height":32,"x":320,"y":192,"pixelRatio":1,"sdf":true,"title":"Travel agent","aliases":["travel agency","holidays","tours"]},
"icon-tube_and_toothbrush": {"width":32,"height":32,"x":320,"y":224,"pixelRatio":1,"sdf":true,"title":"Chemist","aliases":["drugstore","toiletries","health and beauty"]},
"icon-vending_machine": {"width":32,"height":32,"x":320,"y":256,"pixelRatio":1,"sdf":true,"title":"Vending machine","aliases":["vendingmachine","vending","automat"]},
"icon-veterinary": {"width":32,"height":32,"x":320,"y":288,"pixelRatio":1,"sdf":true,"title":"Vet","aliases":["animal hospital","animal clinic","pets"]},
"icon-video": {"width":32,"height":32,"x":0,"y":320,"pixelRatio":1,"sdf":true,"title":"Video shop","aliases":["dvd","video rental","movies"]},
"icon-viewpoint": {"width":32,"height":32,"x":32,"y":320,"pixelRatio":1,"sdf":true,"title":"Viewpoint","aliases":["lookout","vista","panorama","scenic"]},
"icon-waste_basket": {"width":32,"height":32,"x":64,"y":320,"pixelRatio":1,"sdf":true,"title":"Waste basket","aliases":["bin","litter bin","rubbish","trash"]},
"icon-wastewater": {"width":32,"height":32,"x":96,"y":320,"pixelRatio":1,"sdf":true,"title":"Wastewater plant","aliases":["sewage","treatment works","sewer"]},
"icon-water_park": {"width":32,"height":32,"x":128,"y":320,"pixelRatio":1,"sdf":true,"title":"Water park","aliases":["waterpark","aquapark","slides"]},
"icon-watermill": {"width":32,"height":32,"x":160,"y":320,"pixelRatio":1,"sdf":true,"title":"Watermill","aliases":["mill","water wheel"]},
"icon-waterworks": {"width":32,"height":32,"x":192,"y":320,"pixelRatio":1,"sdf":true,"title":"Waterworks","aliases":["water treatment","pumping station","water supply"]},
"icon-windmill": {"width":32,"height":32,"x":224,"y":320,"pixelRatio":1,"sdf":true,"title":"Windmill","aliases":["mill","wind mill"]},
"icon-yin_yang": {"width":32,"height":32,"x":256,"y":320,"pixelRatio":1,"sdf":true,"title":"Yin and yang","aliases":["taoist","taoism","religion","balance"]},
"icon-zoo": {"width":32,"height":32,"x":288,"y":320,"pixelRatio":1,"sdf":true,"title":"Zoo","aliases":["animals","wildlife park","safari"]},
"marking-oneway": {"width":14,"height":25,"x":352,"y":32,"pixelRatio":1,"sdf":true,"title":"One-way arrow","aliases":["one way","direction","traffic flow"],"center":[0.5,0]},
"pattern-hatched": {"width":16,"height":16,"x":366,"y":32,"pixelRatio":1,"sdf":false,"title":"Diagonal hatching","aliases":["hazard","danger area","stripes","fill"]},
"pattern-hatched_thin": {"width":16,"height":16,"x":352,"y":57,"pixelRatio":1,"sdf":false,"title":"Fine diagonal hatching","aliases":["hazard","stripes","fill","light"]},
"pattern-striped": {"width":16,"height":16,"x":368,"y":57,"pixelRatio":1,"sdf":false,"title":"Horizontal stripes","aliases":["bands","lines","fill"]},
"transport-information": {"width":32,"height":32,"x":320,"y":320,"pixelRatio":1,"sdf":true,"title":"Information","aliases":["info","tourist information","help"]},
"transport-tram": {"width":32,"height":32,"x":352,"y":0,"pixelRatio":1,"sdf":true,"title":"Tram","aliases":["streetcar","light rail","trolley"]}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

@@ -0,0 +1,128 @@
{
"icon-airfield": {"width":64,"height":64,"x":0,"y":0,"pixelRatio":2,"sdf":true,"title":"Airfield","aliases":["aerodrome","airstrip","landing strip"]},
"icon-airport": {"width":64,"height":64,"x":64,"y":0,"pixelRatio":2,"sdf":true,"title":"Airport","aliases":["aeroplane","airplane","plane","flight","terminal"]},
"icon-alcohol_shop": {"width":64,"height":64,"x":0,"y":64,"pixelRatio":2,"sdf":true,"title":"Alcohol shop","aliases":["off-licence","liquor store","bottle shop","wine","spirits"]},
"icon-amusement_park": {"width":64,"height":64,"x":64,"y":64,"pixelRatio":2,"sdf":true,"title":"Amusement park","aliases":["theme park","fairground","rides"]},
"icon-art_gallery": {"width":64,"height":64,"x":128,"y":0,"pixelRatio":2,"sdf":true,"title":"Art gallery","aliases":["picture","painting","exhibition"]},
"icon-artwork": {"width":64,"height":64,"x":128,"y":64,"pixelRatio":2,"sdf":true,"title":"Artwork","aliases":["public art","sculpture","installation"]},
"icon-atm": {"width":64,"height":64,"x":0,"y":128,"pixelRatio":2,"sdf":true,"title":"ATM","aliases":["cash machine","cashpoint","bank machine"]},
"icon-bakery": {"width":64,"height":64,"x":64,"y":128,"pixelRatio":2,"sdf":true,"title":"Bakery","aliases":["bread","baker","patisserie"]},
"icon-bank": {"width":64,"height":64,"x":128,"y":128,"pixelRatio":2,"sdf":true,"title":"Bank","aliases":["branch","banking","finance"]},
"icon-bar": {"width":64,"height":64,"x":192,"y":0,"pixelRatio":2,"sdf":true,"title":"Bar","aliases":["cocktail","drinks","lounge"]},
"icon-beauty": {"width":64,"height":64,"x":192,"y":64,"pixelRatio":2,"sdf":true,"title":"Beauty salon","aliases":["cosmetics","nails","spa"]},
"icon-beer_mug": {"width":64,"height":64,"x":192,"y":128,"pixelRatio":2,"sdf":true,"title":"Beer mug","aliases":["beer","beergarden","beer garden","brewery"]},
"icon-bench": {"width":64,"height":64,"x":0,"y":192,"pixelRatio":2,"sdf":true,"title":"Bench","aliases":["seat","seating","rest"]},
"icon-beverages": {"width":64,"height":64,"x":64,"y":192,"pixelRatio":2,"sdf":true,"title":"Drinks shop","aliases":["soft drinks","juice","off-licence"]},
"icon-bicycle_share": {"width":64,"height":64,"x":128,"y":192,"pixelRatio":2,"sdf":true,"title":"Bike share","aliases":["bicycle sharing","hire","docking station"]},
"icon-books": {"width":64,"height":64,"x":192,"y":192,"pixelRatio":2,"sdf":true,"title":"Bookshop","aliases":["bookstore","reading","literature"]},
"icon-bus": {"width":64,"height":64,"x":256,"y":0,"pixelRatio":2,"sdf":true,"title":"Bus","aliases":["coach","omnibus","public transport"]},
"icon-butcher": {"width":64,"height":64,"x":256,"y":64,"pixelRatio":2,"sdf":true,"title":"Butcher","aliases":["meat","deli","carvery"]},
"icon-cafe": {"width":64,"height":64,"x":256,"y":128,"pixelRatio":2,"sdf":true,"title":"Café","aliases":["coffee","coffee shop","espresso","tea"]},
"icon-campsite": {"width":64,"height":64,"x":256,"y":192,"pixelRatio":2,"sdf":true,"title":"Campsite","aliases":["camping","tent","pitch"]},
"icon-car_rental": {"width":64,"height":64,"x":0,"y":256,"pixelRatio":2,"sdf":true,"title":"Car rental","aliases":["car hire","rent a car"]},
"icon-car_wash": {"width":64,"height":64,"x":64,"y":256,"pixelRatio":2,"sdf":true,"title":"Car wash","aliases":["valeting","vehicle wash"]},
"icon-caravan": {"width":64,"height":64,"x":128,"y":256,"pixelRatio":2,"sdf":true,"title":"Caravan site","aliases":["motorhome","rv","campervan"]},
"icon-castle": {"width":64,"height":64,"x":192,"y":256,"pixelRatio":2,"sdf":true,"title":"Castle","aliases":["fort","fortress","palace","chateau"]},
"icon-cemetery": {"width":64,"height":64,"x":256,"y":256,"pixelRatio":2,"sdf":true,"title":"Cemetery","aliases":["graveyard","burial","grave"]},
"icon-chalet": {"width":64,"height":64,"x":320,"y":0,"pixelRatio":2,"sdf":true,"title":"Chalet","aliases":["cabin","lodge","holiday home"]},
"icon-cinema": {"width":64,"height":64,"x":320,"y":64,"pixelRatio":2,"sdf":true,"title":"Cinema","aliases":["movie theater","movies","film"]},
"icon-clothes": {"width":64,"height":64,"x":320,"y":128,"pixelRatio":2,"sdf":true,"title":"Clothes shop","aliases":["clothing","fashion","apparel","boutique"]},
"icon-college": {"width":64,"height":64,"x":320,"y":192,"pixelRatio":2,"sdf":true,"title":"College","aliases":["university","campus","further education"]},
"icon-community": {"width":64,"height":64,"x":320,"y":256,"pixelRatio":2,"sdf":true,"title":"Community centre","aliases":["community center","village hall","meeting"]},
"icon-defibrillator": {"width":64,"height":64,"x":0,"y":320,"pixelRatio":2,"sdf":true,"title":"Defibrillator","aliases":["aed","heart","resuscitation","emergency"]},
"icon-dentist": {"width":64,"height":64,"x":64,"y":320,"pixelRatio":2,"sdf":true,"title":"Dentist","aliases":["dental","teeth","orthodontist"]},
"icon-dharma_wheel": {"width":64,"height":64,"x":128,"y":320,"pixelRatio":2,"sdf":true,"title":"Dharma wheel","aliases":["buddhist","buddhism","temple","religion"]},
"icon-do_it_yourself": {"width":64,"height":64,"x":192,"y":320,"pixelRatio":2,"sdf":true,"title":"DIY store","aliases":["doityourself","home improvement","builders merchant"]},
"icon-doctor": {"width":64,"height":64,"x":256,"y":320,"pixelRatio":2,"sdf":true,"title":"Doctor","aliases":["gp","surgery","clinic","physician"]},
"icon-dog": {"width":64,"height":64,"x":320,"y":320,"pixelRatio":2,"sdf":true,"title":"Dog park","aliases":["dog_park","dogs","pet","off-leash"]},
"icon-drinking_water": {"width":64,"height":64,"x":384,"y":0,"pixelRatio":2,"sdf":true,"title":"Drinking water","aliases":["tap","potable","refill","water point"]},
"icon-dry_cleaning": {"width":64,"height":64,"x":384,"y":64,"pixelRatio":2,"sdf":true,"title":"Dry cleaning","aliases":["drycleaning","cleaner","launderer"]},
"icon-embassy": {"width":64,"height":64,"x":384,"y":128,"pixelRatio":2,"sdf":true,"title":"Embassy","aliases":["consulate","diplomatic","mission"]},
"icon-emergency_access": {"width":64,"height":64,"x":384,"y":192,"pixelRatio":2,"sdf":true,"title":"Emergency access","aliases":["access point","rescue point"]},
"icon-emergency_phone": {"width":64,"height":64,"x":384,"y":256,"pixelRatio":2,"sdf":true,"title":"Emergency phone","aliases":["sos","help point","call box"]},
"icon-fast_food": {"width":64,"height":64,"x":384,"y":320,"pixelRatio":2,"sdf":true,"title":"Fast food","aliases":["takeaway","burger","snack"]},
"icon-fire_station": {"width":64,"height":64,"x":0,"y":384,"pixelRatio":2,"sdf":true,"title":"Fire station","aliases":["fire brigade","fire service","firefighters"]},
"icon-florist": {"width":64,"height":64,"x":64,"y":384,"pixelRatio":2,"sdf":true,"title":"Florist","aliases":["flowers","flower shop","bouquet"]},
"icon-fountain": {"width":64,"height":64,"x":128,"y":384,"pixelRatio":2,"sdf":true,"title":"Fountain","aliases":["water feature","jet"]},
"icon-fuel_pump": {"width":64,"height":64,"x":192,"y":384,"pixelRatio":2,"sdf":true,"title":"Petrol station","aliases":["fuel","gas station","filling station","diesel"]},
"icon-furniture": {"width":64,"height":64,"x":256,"y":384,"pixelRatio":2,"sdf":true,"title":"Furniture shop","aliases":["furnishings","home store"]},
"icon-garden_center": {"width":64,"height":64,"x":320,"y":384,"pixelRatio":2,"sdf":true,"title":"Garden centre","aliases":["garden_centre","nursery","plants"]},
"icon-gift": {"width":64,"height":64,"x":384,"y":384,"pixelRatio":2,"sdf":true,"title":"Gift shop","aliases":["presents","souvenirs"]},
"icon-golf": {"width":64,"height":64,"x":448,"y":0,"pixelRatio":2,"sdf":true,"title":"Golf","aliases":["golf course","driving range"]},
"icon-greengrocer": {"width":64,"height":64,"x":448,"y":64,"pixelRatio":2,"sdf":true,"title":"Greengrocer","aliases":["fruit","vegetables","produce"]},
"icon-hardware": {"width":64,"height":64,"x":448,"y":128,"pixelRatio":2,"sdf":true,"title":"Hardware shop","aliases":["tools","ironmonger"]},
"icon-historic": {"width":64,"height":64,"x":448,"y":192,"pixelRatio":2,"sdf":true,"title":"Historic site","aliases":["heritage","ruins","antiquity"]},
"icon-hospital": {"width":64,"height":64,"x":448,"y":256,"pixelRatio":2,"sdf":true,"title":"Hospital","aliases":["a&e","emergency room","infirmary","medical"]},
"icon-hunting_stand": {"width":64,"height":64,"x":448,"y":320,"pixelRatio":2,"sdf":true,"title":"Hunting stand","aliases":["huntingstand","hide","deer stand"]},
"icon-hydrant": {"width":64,"height":64,"x":448,"y":384,"pixelRatio":2,"sdf":true,"title":"Fire hydrant","aliases":["fire plug","water point","firefighting"]},
"icon-ice_rink": {"width":64,"height":64,"x":0,"y":448,"pixelRatio":2,"sdf":true,"title":"Ice rink","aliases":["icerink","skating","ice skating"]},
"icon-justice": {"width":64,"height":64,"x":64,"y":448,"pixelRatio":2,"sdf":true,"title":"Courthouse","aliases":["court","law court","tribunal","judiciary"]},
"icon-khanda": {"width":64,"height":64,"x":128,"y":448,"pixelRatio":2,"sdf":true,"title":"Khanda","aliases":["sikh","sikhism","gurdwara","religion"]},
"icon-latin_cross": {"width":64,"height":64,"x":192,"y":448,"pixelRatio":2,"sdf":true,"title":"Latin cross","aliases":["christian","christianity","church","religion"]},
"icon-laundry": {"width":64,"height":64,"x":256,"y":448,"pixelRatio":2,"sdf":true,"title":"Laundry","aliases":["launderette","laundromat","washing"]},
"icon-library": {"width":64,"height":64,"x":320,"y":448,"pixelRatio":2,"sdf":true,"title":"Library","aliases":["lending","public library","archive"]},
"icon-lighthouse": {"width":64,"height":64,"x":384,"y":448,"pixelRatio":2,"sdf":true,"title":"Lighthouse","aliases":["beacon","navigation","coast"]},
"icon-lodging": {"width":64,"height":64,"x":448,"y":448,"pixelRatio":2,"sdf":true,"title":"Hotel","aliases":["accommodation","guest house","hostel","motel","b&b"]},
"icon-marketplace": {"width":64,"height":64,"x":512,"y":0,"pixelRatio":2,"sdf":true,"title":"Marketplace","aliases":["market","stalls","bazaar"]},
"icon-monument": {"width":64,"height":64,"x":512,"y":64,"pixelRatio":2,"sdf":true,"title":"Monument","aliases":["memorial","statue","landmark"]},
"icon-newsagent": {"width":64,"height":64,"x":512,"y":128,"pixelRatio":2,"sdf":true,"title":"Newsagent","aliases":["news","tobacconist","corner shop"]},
"icon-newspaper": {"width":64,"height":64,"x":512,"y":192,"pixelRatio":2,"sdf":true,"title":"Newspaper kiosk","aliases":["kiosk","newsstand","press"]},
"icon-nightclub": {"width":64,"height":64,"x":512,"y":256,"pixelRatio":2,"sdf":true,"title":"Nightclub","aliases":["club","disco","dancing"]},
"icon-nursing_home": {"width":64,"height":64,"x":512,"y":320,"pixelRatio":2,"sdf":true,"title":"Nursing home","aliases":["nursinghome","care home","elderly"]},
"icon-observation_tower": {"width":64,"height":64,"x":512,"y":384,"pixelRatio":2,"sdf":true,"title":"Observation tower","aliases":["lookout","viewing tower","watchtower"]},
"icon-om": {"width":64,"height":64,"x":512,"y":448,"pixelRatio":2,"sdf":true,"title":"Om","aliases":["hindu","hinduism","temple","religion"]},
"icon-optician": {"width":64,"height":64,"x":0,"y":512,"pixelRatio":2,"sdf":true,"title":"Optician","aliases":["glasses","eyewear","optometrist"]},
"icon-outdoor": {"width":64,"height":64,"x":64,"y":512,"pixelRatio":2,"sdf":true,"title":"Outdoor shop","aliases":["camping shop","hiking","mountaineering"]},
"icon-person_kneeling_and_praying": {"width":64,"height":64,"x":128,"y":512,"pixelRatio":2,"sdf":true,"title":"Place of worship","aliases":["place_of_worship","prayer","worship","religion"]},
"icon-picnic_site": {"width":64,"height":64,"x":192,"y":512,"pixelRatio":2,"sdf":true,"title":"Picnic site","aliases":["picnic","picnic table","rest area"]},
"icon-pill": {"width":64,"height":64,"x":256,"y":512,"pixelRatio":2,"sdf":true,"title":"Pharmacy","aliases":["chemist","drugstore","medicine","prescription"]},
"icon-pint_glass": {"width":64,"height":64,"x":320,"y":512,"pixelRatio":2,"sdf":true,"title":"Pub","aliases":["public house","tavern","inn","beer"]},
"icon-pitch": {"width":64,"height":64,"x":384,"y":512,"pixelRatio":2,"sdf":true,"title":"Sports pitch","aliases":["field","court","playing field"]},
"icon-police_officer": {"width":64,"height":64,"x":448,"y":512,"pixelRatio":2,"sdf":true,"title":"Police","aliases":["police station","law enforcement","constabulary"]},
"icon-post": {"width":64,"height":64,"x":512,"y":512,"pixelRatio":2,"sdf":true,"title":"Post office","aliases":["mail","postal","parcel"]},
"icon-postbox": {"width":64,"height":64,"x":576,"y":0,"pixelRatio":2,"sdf":true,"title":"Post box","aliases":["mailbox","letter box","pillar box"]},
"icon-prison": {"width":64,"height":64,"x":576,"y":64,"pixelRatio":2,"sdf":true,"title":"Prison","aliases":["jail","correctional","penitentiary"]},
"icon-rail": {"width":64,"height":64,"x":576,"y":128,"pixelRatio":2,"sdf":true,"title":"Railway","aliases":["train","railway station","metro","subway"]},
"icon-recycling": {"width":64,"height":64,"x":576,"y":192,"pixelRatio":2,"sdf":true,"title":"Recycling","aliases":["bottle bank","waste","recycle centre"]},
"icon-restaurant": {"width":64,"height":64,"x":576,"y":256,"pixelRatio":2,"sdf":true,"title":"Restaurant","aliases":["dining","eat","food","bistro"]},
"icon-restrooms": {"width":64,"height":64,"x":576,"y":320,"pixelRatio":2,"sdf":true,"title":"Toilets","aliases":["toilet","wc","bathroom","lavatory"]},
"icon-ring": {"width":64,"height":64,"x":576,"y":384,"pixelRatio":2,"sdf":true,"title":"Jewellery shop","aliases":["jewelry_store","jeweller","jewelry","rings"]},
"icon-rocking_horse": {"width":64,"height":64,"x":576,"y":448,"pixelRatio":2,"sdf":true,"title":"Toy shop","aliases":["toys","toy store","games"]},
"icon-school": {"width":64,"height":64,"x":576,"y":512,"pixelRatio":2,"sdf":true,"title":"School","aliases":["primary","secondary","education"]},
"icon-scissors_and_comb": {"width":64,"height":64,"x":0,"y":576,"pixelRatio":2,"sdf":true,"title":"Hairdresser","aliases":["barber","salon","haircut","hair"]},
"icon-seesaw": {"width":64,"height":64,"x":64,"y":576,"pixelRatio":2,"sdf":true,"title":"Playground","aliases":["play area","swings","children"]},
"icon-shelter": {"width":64,"height":64,"x":128,"y":576,"pixelRatio":2,"sdf":true,"title":"Shelter","aliases":["bus shelter","refuge","hut"]},
"icon-shoes": {"width":64,"height":64,"x":192,"y":576,"pixelRatio":2,"sdf":true,"title":"Shoe shop","aliases":["footwear","cobbler","trainers"]},
"icon-shop": {"width":64,"height":64,"x":256,"y":576,"pixelRatio":2,"sdf":true,"title":"Shop","aliases":["store","retail","convenience"]},
"icon-shrine": {"width":64,"height":64,"x":320,"y":576,"pixelRatio":2,"sdf":true,"title":"Shrine","aliases":["wayside shrine","sacred","religion"]},
"icon-sports": {"width":64,"height":64,"x":384,"y":576,"pixelRatio":2,"sdf":true,"title":"Sports centre","aliases":["leisure centre","gym","fitness"]},
"icon-stadium": {"width":64,"height":64,"x":448,"y":576,"pixelRatio":2,"sdf":true,"title":"Stadium","aliases":["arena","ground","sports ground"]},
"icon-star_and_crescent": {"width":64,"height":64,"x":512,"y":576,"pixelRatio":2,"sdf":true,"title":"Star and crescent","aliases":["muslim","islam","mosque","religion"]},
"icon-star_of_david": {"width":64,"height":64,"x":576,"y":576,"pixelRatio":2,"sdf":true,"title":"Star of David","aliases":["jewish","judaism","synagogue","religion"]},
"icon-stationery": {"width":64,"height":64,"x":640,"y":0,"pixelRatio":2,"sdf":true,"title":"Stationery shop","aliases":["office supplies","pens","paper"]},
"icon-surveillance": {"width":64,"height":64,"x":640,"y":64,"pixelRatio":2,"sdf":true,"title":"Surveillance camera","aliases":["cctv","camera","security"]},
"icon-swimming": {"width":64,"height":64,"x":640,"y":128,"pixelRatio":2,"sdf":true,"title":"Swimming","aliases":["pool","swimming pool","lido","baths"]},
"icon-telephone": {"width":64,"height":64,"x":640,"y":192,"pixelRatio":2,"sdf":true,"title":"Telephone","aliases":["phone","call box","payphone"]},
"icon-theater": {"width":64,"height":64,"x":640,"y":256,"pixelRatio":2,"sdf":true,"title":"Theatre","aliases":["playhouse","drama","stage","performing arts"]},
"icon-town_hall": {"width":64,"height":64,"x":640,"y":320,"pixelRatio":2,"sdf":true,"title":"Town hall","aliases":["city hall","civic","municipal"]},
"icon-travel_agent": {"width":64,"height":64,"x":640,"y":384,"pixelRatio":2,"sdf":true,"title":"Travel agent","aliases":["travel agency","holidays","tours"]},
"icon-tube_and_toothbrush": {"width":64,"height":64,"x":640,"y":448,"pixelRatio":2,"sdf":true,"title":"Chemist","aliases":["drugstore","toiletries","health and beauty"]},
"icon-vending_machine": {"width":64,"height":64,"x":640,"y":512,"pixelRatio":2,"sdf":true,"title":"Vending machine","aliases":["vendingmachine","vending","automat"]},
"icon-veterinary": {"width":64,"height":64,"x":640,"y":576,"pixelRatio":2,"sdf":true,"title":"Vet","aliases":["animal hospital","animal clinic","pets"]},
"icon-video": {"width":64,"height":64,"x":0,"y":640,"pixelRatio":2,"sdf":true,"title":"Video shop","aliases":["dvd","video rental","movies"]},
"icon-viewpoint": {"width":64,"height":64,"x":64,"y":640,"pixelRatio":2,"sdf":true,"title":"Viewpoint","aliases":["lookout","vista","panorama","scenic"]},
"icon-waste_basket": {"width":64,"height":64,"x":128,"y":640,"pixelRatio":2,"sdf":true,"title":"Waste basket","aliases":["bin","litter bin","rubbish","trash"]},
"icon-wastewater": {"width":64,"height":64,"x":192,"y":640,"pixelRatio":2,"sdf":true,"title":"Wastewater plant","aliases":["sewage","treatment works","sewer"]},
"icon-water_park": {"width":64,"height":64,"x":256,"y":640,"pixelRatio":2,"sdf":true,"title":"Water park","aliases":["waterpark","aquapark","slides"]},
"icon-watermill": {"width":64,"height":64,"x":320,"y":640,"pixelRatio":2,"sdf":true,"title":"Watermill","aliases":["mill","water wheel"]},
"icon-waterworks": {"width":64,"height":64,"x":384,"y":640,"pixelRatio":2,"sdf":true,"title":"Waterworks","aliases":["water treatment","pumping station","water supply"]},
"icon-windmill": {"width":64,"height":64,"x":448,"y":640,"pixelRatio":2,"sdf":true,"title":"Windmill","aliases":["mill","wind mill"]},
"icon-yin_yang": {"width":64,"height":64,"x":512,"y":640,"pixelRatio":2,"sdf":true,"title":"Yin and yang","aliases":["taoist","taoism","religion","balance"]},
"icon-zoo": {"width":64,"height":64,"x":576,"y":640,"pixelRatio":2,"sdf":true,"title":"Zoo","aliases":["animals","wildlife park","safari"]},
"marking-oneway": {"width":28,"height":50,"x":704,"y":64,"pixelRatio":2,"sdf":true,"title":"One-way arrow","aliases":["one way","direction","traffic flow"],"center":[0.5,0]},
"pattern-hatched": {"width":32,"height":32,"x":732,"y":64,"pixelRatio":2,"sdf":false,"title":"Diagonal hatching","aliases":["hazard","danger area","stripes","fill"]},
"pattern-hatched_thin": {"width":32,"height":32,"x":704,"y":114,"pixelRatio":2,"sdf":false,"title":"Fine diagonal hatching","aliases":["hazard","stripes","fill","light"]},
"pattern-striped": {"width":32,"height":32,"x":736,"y":114,"pixelRatio":2,"sdf":false,"title":"Horizontal stripes","aliases":["bands","lines","fill"]},
"transport-information": {"width":64,"height":64,"x":640,"y":640,"pixelRatio":2,"sdf":true,"title":"Information","aliases":["info","tourist information","help"]},
"transport-tram": {"width":64,"height":64,"x":704,"y":0,"pixelRatio":2,"sdf":true,"title":"Tram","aliases":["streetcar","light rail","trolley"]}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

+9 -15
View File
@@ -69,20 +69,15 @@ export const supportsWebGL2 = (): boolean => {
return webGL2Supported;
};
// MapLibre rejects a relative sprite URL. Not the glyph URL: encoding would
// mangle its {fontstack} and {range} placeholders.
// The demo has no core to proxy through. OSM sets CORS on its tiles but not on
// its glyphs and sprites, which is why those come from VersaTiles.
// its glyphs, which is why those come from VersaTiles.
const DEMO_UPSTREAM = {
tilejson: "https://vector.openstreetmap.org/shortbread_v1/tilejson.json",
assets: "https://tiles.versatiles.org/assets",
glyphs: "https://tiles.versatiles.org/assets/glyphs/{fontstack}/{range}.pbf",
};
const useDemoUpstream = (style: StyleSpecification): StyleSpecification => {
style.glyphs = `${DEMO_UPSTREAM.assets}/glyphs/{fontstack}/{range}.pbf`;
style.sprite = [
{ id: "basics", url: `${DEMO_UPSTREAM.assets}/sprites/basics/sprites` },
];
style.glyphs = DEMO_UPSTREAM.glyphs;
Object.values(style.sources).forEach((source) => {
if ("url" in source) {
source.url = DEMO_UPSTREAM.tilejson;
@@ -91,22 +86,21 @@ const useDemoUpstream = (style: StyleSpecification): StyleSpecification => {
return style;
};
// MapLibre rejects a relative sprite URL
const absoluteSprite = (url: string) => new URL(url, location.href).href;
export const loadStyle = async (url: string): Promise<StyleSpecification> => {
const style: StyleSpecification = await (await fetch(url)).json();
if (__DEMO__) {
return useDemoUpstream(style);
}
if (typeof style.sprite === "string") {
style.sprite = mapTilesUrl(style.sprite);
style.sprite = absoluteSprite(style.sprite);
} else if (Array.isArray(style.sprite)) {
style.sprite = style.sprite.map((sprite) => ({
...sprite,
url: mapTilesUrl(sprite.url),
url: absoluteSprite(sprite.url),
}));
}
return style;
return __DEMO__ ? useDemoUpstream(style) : style;
};
// Global to MapLibre, and it throws when set twice.
+1 -8
View File
@@ -720,14 +720,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
// costs the user their place in the tab order, so stay programmatically
// focusable for as long as we hold focus, however we stop being sonifiable.
this._sonificationFocusHeld = true;
// Clicks and taps focus the chart too. Chart2Music only responds to the
// keyboard, and once connected it moves the tooltip to the first point, so
// pointer focus must not start it.
if (
this._sonification ||
this._sonificationLoading ||
!this._chartContainer?.matches(":focus-visible")
) {
if (this._sonification || this._sonificationLoading) {
return;
}
await this._applyDeferredWork();
+5 -1
View File
@@ -119,7 +119,11 @@ class HaUserPicker extends LitElement {
private _getItems = () => this._getUsers(this.users);
protected render(): TemplateResult {
protected render(): TemplateResult | typeof nothing {
if (!this.users) {
return nothing;
}
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.user-picker.user");
+2
View File
@@ -17,6 +17,7 @@ export interface AssistPipeline {
tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
user_id?: string | null;
}
export interface AssistDevice {
@@ -37,6 +38,7 @@ export interface AssistPipelineMutableParams {
tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
user_id?: string | null;
}
export interface AssistRunListing {
@@ -1,8 +1,10 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import "../../../../components/user/ha-user-picker";
import type { AssistPipeline } from "../../../../data/assist_pipeline";
import type { HomeAssistant } from "../../../../types";
@@ -80,10 +82,34 @@ export class AssistPipelineDetailConfig extends LitElement {
.hass=${this.hass}
.computeLabel=${this._computeLabel}
></ha-form>
<ha-user-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.form.user_id"
)}
.value=${this.data?.user_id ?? ""}
@value-changed=${this._userChanged}
></ha-user-picker>
<p class="user-description">
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.form.user_id_description"
)}
</p>
</div>
`;
}
private _userChanged(ev: CustomEvent) {
ev.stopPropagation();
const userId = ev.detail.value || null;
if (userId === (this.data?.user_id ?? null)) {
return;
}
fireEvent(this, "value-changed", {
value: { ...this.data, user_id: userId },
});
}
static styles = css`
.section {
border: 1px solid var(--divider-color);
@@ -107,6 +133,13 @@ export class AssistPipelineDetailConfig extends LitElement {
margin-top: 0;
margin-bottom: 0;
}
ha-user-picker {
display: block;
margin-top: 16px;
}
p.user-description {
margin-top: 8px;
}
`;
}
@@ -183,7 +183,7 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
.hass=${this.hass}
.data=${this._data}
.supportedLanguages=${this._supportedLanguages}
keys="name,language"
keys="name,language,user_id"
@value-changed=${this._valueChanged}
?autofocus=${!isExistingPipeline}
></assist-pipeline-detail-config>
@@ -301,6 +301,7 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
tts_voice: data.tts_voice ?? null,
wake_word_entity: data.wake_word_entity ?? null,
wake_word_id: data.wake_word_id ?? null,
user_id: data.user_id ?? null,
};
if (
this._params!.pipeline &&
+3 -1
View File
@@ -4955,7 +4955,9 @@
"tts_language": "[%key:ui::panel::config::voice_assistants::assistants::pipeline::detail::form::language%]",
"tts_voice": "Voice",
"wake_word_entity": "Wake word engine",
"wake_word_id": "Wake word"
"wake_word_id": "Wake word",
"user_id": "Acts as user",
"user_id_description": "Voice requests have no user of their own. Pick one and this assistant speaks for them, so integrations that link a Home Assistant user to an account of their own use the right account."
},
"steps": {
"config": {
+68
View File
@@ -0,0 +1,68 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from "vitest";
import { missingSprites } from "../../build-scripts/gulp/map-sprites.js";
const STYLE = {
layers: [
{ id: "poi", type: "symbol", layout: { "icon-image": "base:icon-cafe" } },
{
id: "poi-amenity",
type: "symbol",
layout: {
"icon-image": [
"match",
["get", "amenity"],
"clinic",
"base:icon-hospital",
"base:icon-shop",
],
"text-field": "base:not-an-icon-but-a-label",
},
},
{
id: "water",
type: "fill",
paint: { "fill-pattern": "base:pattern-wave" },
},
{
id: "rail",
type: "line",
paint: {
"line-pattern": [
"case",
["get", "tunnel"],
"base:pattern-rail",
"base:icon-shop",
],
},
},
{
id: "extras",
type: "symbol",
layout: { "icon-image": "extras:shape-star" },
},
],
};
describe("missingSprites", () => {
it("reports every base image the sheet lacks, from literals and expressions", () => {
expect(
missingSprites(STYLE, { "icon-cafe": {}, "icon-shop": {} }).sort()
).toEqual(["icon-hospital", "pattern-rail", "pattern-wave"]);
});
it("ignores base-prefixed text and other sprite sheets", () => {
expect(
missingSprites(STYLE, {
"icon-cafe": {},
"icon-hospital": {},
"icon-shop": {},
"pattern-rail": {},
"pattern-wave": {},
})
).toEqual([]);
});
});
+48 -1
View File
@@ -46,7 +46,7 @@ const STYLE = {
version: 8,
sources: {},
layers: [],
sprite: [{ id: "basics", url: "/static/map/sprites/basics/sprites" }],
sprite: [{ id: "base", url: "/static/map/sprites/base?v=abc12345" }],
};
const rasterLayer = {
@@ -104,6 +104,53 @@ afterEach(() => {
vi.restoreAllMocks();
});
describe("loadStyle", () => {
const load = async () =>
(await import("../../../src/common/map/base-layer")).loadStyle(
"/static/map/light.json"
);
it("makes the bundled sprite URL absolute against this page", async () => {
const style = await load();
expect(style.sprite).toEqual([
{
id: "base",
url: `${location.origin}/static/map/sprites/base?v=abc12345`,
},
]);
});
it("keeps the bundled sprites in the demo, with tiles and glyphs upstream", async () => {
vi.stubGlobal("__DEMO__", true);
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
json: async () => ({
...structuredClone(STYLE),
glyphs: "/api/map_tiles/fonts/{fontstack}/{range}.pbf",
sources: {
osm: { type: "vector", url: "/api/map_tiles/tilejson.json" },
},
}),
}))
);
const style = await load();
expect(style.sprite).toEqual([
{
id: "base",
url: `${location.origin}/static/map/sprites/base?v=abc12345`,
},
]);
expect(style.glyphs).toMatch(/^https:\/\/tiles\.versatiles\.org\//);
expect((style.sources.osm as { url: string }).url).toMatch(
/^https:\/\/vector\.openstreetmap\.org\//
);
});
});
describe("createBaseLayer", () => {
it("falls back to raster tiles without WebGL2", async () => {
const createBaseLayer = await setWebGL2(false);
+100
View File
@@ -0,0 +1,100 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import "../../src/components/user/ha-user-picker";
import type { User } from "../../src/data/user";
import type { HomeAssistant } from "../../src/types";
const USERS = [
{
id: "user-firstuser",
username: "firstuser",
name: "First User",
is_owner: false,
is_active: true,
local_only: false,
system_generated: false,
group_ids: [],
credentials: [],
},
] as unknown as User[];
const hassWithUsers = (users: User[]) =>
({
localize: (key: string) => key,
callWS: () => Promise.resolve(users),
}) as unknown as HomeAssistant;
const headlineText = (el: HTMLElement): string => {
const texts: string[] = [];
const walk = (root: ParentNode) => {
for (const node of Array.from(root.querySelectorAll("*"))) {
if (node.getAttribute("slot") === "headline") {
texts.push(node.textContent?.trim() ?? "");
}
if (node.shadowRoot) {
walk(node.shadowRoot);
}
}
};
walk(el.shadowRoot!);
return texts.join("|");
};
const tick = () =>
new Promise((resolve) => {
setTimeout(resolve, 0);
});
// the picker fetches its users, so let its update and that fetch both land
const settle = async (el: any) => {
await Promise.all([el.updateComplete, tick()]);
await Promise.all([el.updateComplete, tick()]);
await el.updateComplete;
};
describe("ha-user-picker", () => {
beforeAll(() => {
// ha-generic-picker asks for it while connecting, jsdom does not provide it
window.matchMedia = vi.fn().mockReturnValue({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}) as any;
const internalsProto = window.ElementInternals.prototype as any;
internalsProto.setValidity = vi.fn();
internalsProto.setFormValue = vi.fn();
Object.defineProperty(internalsProto, "validity", {
get: () => ({ valid: true }),
configurable: true,
});
});
afterEach(() => {
document.body.innerHTML = "";
});
it("names the user its value refers to", async () => {
const el = document.createElement("ha-user-picker") as any;
el.hass = hassWithUsers(USERS);
el.users = USERS;
el.value = "user-firstuser";
document.body.appendChild(el);
await settle(el);
expect(headlineText(el)).toContain("First User");
});
it("names the user once the fetched list arrives", async () => {
// A standalone picker fetches its own users, so it is first asked to render
// a value it cannot resolve yet. It must not settle on the raw user id.
const el = document.createElement("ha-user-picker") as any;
el.hass = hassWithUsers(USERS);
el.value = "user-firstuser";
document.body.appendChild(el);
await settle(el);
expect(headlineText(el)).toContain("First User");
expect(headlineText(el)).not.toContain("user-firstuser");
});
});
+31 -8
View File
@@ -3546,7 +3546,7 @@ __metadata:
languageName: node
linkType: hard
"@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2":
"@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:^2.0.3, @mapbox/jsonlint-lines-primitives@npm:~2.0.2":
version: 2.0.3
resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.3"
checksum: 10/99404df11b1840153c910154c238f62ba0ea7cfab6aa7ed3bad1e522d31b746bfad78b23e6ee4887196acec76759f1863b5cb4f5e2d95d0dc2a8ea73091c78f6
@@ -3644,6 +3644,24 @@ __metadata:
languageName: node
linkType: hard
"@maplibre/maplibre-gl-style-spec@npm:^26.4.4":
version: 26.4.4
resolution: "@maplibre/maplibre-gl-style-spec@npm:26.4.4"
dependencies:
"@mapbox/jsonlint-lines-primitives": "npm:^2.0.3"
"@mapbox/unitbezier": "npm:^1.0.0"
json-stringify-pretty-compact: "npm:^4.0.0"
minimist: "npm:^1.2.8"
quickselect: "npm:^3.0.0"
tinyqueue: "npm:^3.0.0"
bin:
gl-style-format: dist/gl-style-format.mjs
gl-style-migrate: dist/gl-style-migrate.mjs
gl-style-validate: dist/gl-style-validate.mjs
checksum: 10/c049485af7654f7ffef63cb0ba7afbae231225151fcf0de173f03815e5b3edac5182c94287b0871a77afef0c95537b8d06da6142aa04eecb541430016e368972
languageName: node
linkType: hard
"@maplibre/mlt@npm:^1.1.8":
version: 1.2.0
resolution: "@maplibre/mlt@npm:1.2.0"
@@ -6706,12 +6724,17 @@ __metadata:
languageName: node
linkType: hard
"@versatiles/style@npm:5.13.1":
version: 5.13.1
resolution: "@versatiles/style@npm:5.13.1"
"@versatiles/style@npm:6.0.1":
version: 6.0.1
resolution: "@versatiles/style@npm:6.0.1"
dependencies:
brace-expansion: "npm:^5.0.9"
checksum: 10/046be455df89f55bc6a8b1b63a01169f68e701799249761c49523c6098b871d9cdb5b0c8a6a1a15503eee17d89f6fb9f0421eb48cf71d70dfa2cee2900a5b25a
"@maplibre/maplibre-gl-style-spec": "npm:^26.4.4"
peerDependencies:
maplibre-gl: ">=5.0.0"
peerDependenciesMeta:
maplibre-gl:
optional: true
checksum: 10/6a4c2c0496c7ab7034eedb91946d60df54a2dbb0b739c86fbb92fa91ac47825434c38892450c0b82282019728ff6f8840b19f3c20d55a67bf70357766040efe2
languageName: node
linkType: hard
@@ -7655,7 +7678,7 @@ __metadata:
languageName: node
linkType: hard
"brace-expansion@npm:^5.0.8, brace-expansion@npm:^5.0.9":
"brace-expansion@npm:^5.0.8":
version: 5.0.9
resolution: "brace-expansion@npm:5.0.9"
dependencies:
@@ -10415,7 +10438,7 @@ __metadata:
"@types/sortablejs": "npm:1.15.9"
"@types/tar": "npm:7.0.87"
"@typescript/native": "npm:[email protected]"
"@versatiles/style": "npm:5.13.1"
"@versatiles/style": "npm:6.0.1"
"@vibrant/color": "npm:4.0.4"
"@vitest/coverage-v8": "npm:5.0.2"
"@vvo/tzdb": "npm:6.198.0"