diff --git a/bun.lock b/bun.lock index a51badf7..97a14cb1 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "bun-react-template", "dependencies": { + "@iconify/react": "^6.0.0", "@mui/icons-material": "^7.3.1", "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0", @@ -40,6 +41,10 @@ "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + "@iconify/react": ["@iconify/react@6.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-eqNscABVZS8eCpZLU/L5F5UokMS9mnCf56iS1nM9YYHdH8ZxqZL9zyjSwW60IOQFsXZkilbBiv+1paMXBhSQnw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@7.3.1", "", {}, "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ=="], "@mui/icons-material": ["@mui/icons-material@7.3.1", "", { "dependencies": { "@babel/runtime": "^7.28.2" }, "peerDependencies": { "@mui/material": "^7.3.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-upzCtG6awpL6noEZlJ5Z01khZ9VnLNLaj7tb6iPbN6G97eYfUTs8e9OyPKy3rEms3VQWmVBfri7jzeaRxdFIzA=="], diff --git a/node_modules/@iconify/react/dist/iconify.cjs b/node_modules/@iconify/react/dist/iconify.cjs new file mode 100644 index 00000000..32b19910 --- /dev/null +++ b/node_modules/@iconify/react/dist/iconify.cjs @@ -0,0 +1,1904 @@ +'use client'; + +'use strict'; + +var react = require('react'); + +const defaultIconDimensions = Object.freeze( + { + left: 0, + top: 0, + width: 16, + height: 16 + } +); +const defaultIconTransformations = Object.freeze({ + rotate: 0, + vFlip: false, + hFlip: false +}); +const defaultIconProps = Object.freeze({ + ...defaultIconDimensions, + ...defaultIconTransformations +}); +const defaultExtendedIconProps = Object.freeze({ + ...defaultIconProps, + body: "", + hidden: false +}); + +function mergeIconTransformations(obj1, obj2) { + const result = {}; + if (!obj1.hFlip !== !obj2.hFlip) { + result.hFlip = true; + } + if (!obj1.vFlip !== !obj2.vFlip) { + result.vFlip = true; + } + const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4; + if (rotate) { + result.rotate = rotate; + } + return result; +} + +function mergeIconData(parent, child) { + const result = mergeIconTransformations(parent, child); + for (const key in defaultExtendedIconProps) { + if (key in defaultIconTransformations) { + if (key in parent && !(key in result)) { + result[key] = defaultIconTransformations[key]; + } + } else if (key in child) { + result[key] = child[key]; + } else if (key in parent) { + result[key] = parent[key]; + } + } + return result; +} + +function getIconsTree(data, names) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + const resolved = /* @__PURE__ */ Object.create(null); + function resolve(name) { + if (icons[name]) { + return resolved[name] = []; + } + if (!(name in resolved)) { + resolved[name] = null; + const parent = aliases[name] && aliases[name].parent; + const value = parent && resolve(parent); + if (value) { + resolved[name] = [parent].concat(value); + } + } + return resolved[name]; + } + (Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve); + return resolved; +} + +function internalGetIconData(data, name, tree) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + let currentProps = {}; + function parse(name2) { + currentProps = mergeIconData( + icons[name2] || aliases[name2], + currentProps + ); + } + parse(name); + tree.forEach(parse); + return mergeIconData(data, currentProps); +} + +function parseIconSet(data, callback) { + const names = []; + if (typeof data !== "object" || typeof data.icons !== "object") { + return names; + } + if (data.not_found instanceof Array) { + data.not_found.forEach((name) => { + callback(name, null); + names.push(name); + }); + } + const tree = getIconsTree(data); + for (const name in tree) { + const item = tree[name]; + if (item) { + callback(name, internalGetIconData(data, name, item)); + names.push(name); + } + } + return names; +} + +const optionalPropertyDefaults = { + provider: "", + aliases: {}, + not_found: {}, + ...defaultIconDimensions +}; +function checkOptionalProps(item, defaults) { + for (const prop in defaults) { + if (prop in item && typeof item[prop] !== typeof defaults[prop]) { + return false; + } + } + return true; +} +function quicklyValidateIconSet(obj) { + if (typeof obj !== "object" || obj === null) { + return null; + } + const data = obj; + if (typeof data.prefix !== "string" || !obj.icons || typeof obj.icons !== "object") { + return null; + } + if (!checkOptionalProps(obj, optionalPropertyDefaults)) { + return null; + } + const icons = data.icons; + for (const name in icons) { + const icon = icons[name]; + if ( + // Name cannot be empty + !name || // Must have body + typeof icon.body !== "string" || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + for (const name in aliases) { + const icon = aliases[name]; + const parent = icon.parent; + if ( + // Name cannot be empty + !name || // Parent must be set and point to existing icon + typeof parent !== "string" || !icons[parent] && !aliases[parent] || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + return data; +} + +const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const stringToIcon = (value, validate, allowSimpleName, provider = "") => { + const colonSeparated = value.split(":"); + if (value.slice(0, 1) === "@") { + if (colonSeparated.length < 2 || colonSeparated.length > 3) { + return null; + } + provider = colonSeparated.shift().slice(1); + } + if (colonSeparated.length > 3 || !colonSeparated.length) { + return null; + } + if (colonSeparated.length > 1) { + const name2 = colonSeparated.pop(); + const prefix = colonSeparated.pop(); + const result = { + // Allow provider without '@': "provider:prefix:name" + provider: colonSeparated.length > 0 ? colonSeparated[0] : provider, + prefix, + name: name2 + }; + return validate && !validateIconName(result) ? null : result; + } + const name = colonSeparated[0]; + const dashSeparated = name.split("-"); + if (dashSeparated.length > 1) { + const result = { + provider, + prefix: dashSeparated.shift(), + name: dashSeparated.join("-") + }; + return validate && !validateIconName(result) ? null : result; + } + if (allowSimpleName && provider === "") { + const result = { + provider, + prefix: "", + name + }; + return validate && !validateIconName(result, allowSimpleName) ? null : result; + } + return null; +}; +const validateIconName = (icon, allowSimpleName) => { + if (!icon) { + return false; + } + return !!// Check prefix: cannot be empty, unless allowSimpleName is enabled + // Check name: cannot be empty + ((allowSimpleName && icon.prefix === "" || !!icon.prefix) && !!icon.name); +}; + +const dataStorage = /* @__PURE__ */ Object.create(null); +function newStorage(provider, prefix) { + return { + provider, + prefix, + icons: /* @__PURE__ */ Object.create(null), + missing: /* @__PURE__ */ new Set() + }; +} +function getStorage(provider, prefix) { + const providerStorage = dataStorage[provider] || (dataStorage[provider] = /* @__PURE__ */ Object.create(null)); + return providerStorage[prefix] || (providerStorage[prefix] = newStorage(provider, prefix)); +} +function addIconSet(storage, data) { + if (!quicklyValidateIconSet(data)) { + return []; + } + return parseIconSet(data, (name, icon) => { + if (icon) { + storage.icons[name] = icon; + } else { + storage.missing.add(name); + } + }); +} +function addIconToStorage(storage, name, icon) { + try { + if (typeof icon.body === "string") { + storage.icons[name] = { ...icon }; + return true; + } + } catch (err) { + } + return false; +} +function listIcons(provider, prefix) { + let allIcons = []; + const providers = typeof provider === "string" ? [provider] : Object.keys(dataStorage); + providers.forEach((provider2) => { + const prefixes = typeof provider2 === "string" && typeof prefix === "string" ? [prefix] : Object.keys(dataStorage[provider2] || {}); + prefixes.forEach((prefix2) => { + const storage = getStorage(provider2, prefix2); + allIcons = allIcons.concat( + Object.keys(storage.icons).map( + (name) => (provider2 !== "" ? "@" + provider2 + ":" : "") + prefix2 + ":" + name + ) + ); + }); + }); + return allIcons; +} + +let simpleNames = false; +function allowSimpleNames(allow) { + if (typeof allow === "boolean") { + simpleNames = allow; + } + return simpleNames; +} +function getIconData(name) { + const icon = typeof name === "string" ? stringToIcon(name, true, simpleNames) : name; + if (icon) { + const storage = getStorage(icon.provider, icon.prefix); + const iconName = icon.name; + return storage.icons[iconName] || (storage.missing.has(iconName) ? null : void 0); + } +} +function addIcon(name, data) { + const icon = stringToIcon(name, true, simpleNames); + if (!icon) { + return false; + } + const storage = getStorage(icon.provider, icon.prefix); + if (data) { + return addIconToStorage(storage, icon.name, data); + } else { + storage.missing.add(icon.name); + return true; + } +} +function addCollection(data, provider) { + if (typeof data !== "object") { + return false; + } + if (typeof provider !== "string") { + provider = data.provider || ""; + } + if (simpleNames && !provider && !data.prefix) { + let added = false; + if (quicklyValidateIconSet(data)) { + data.prefix = ""; + parseIconSet(data, (name, icon) => { + if (addIcon(name, icon)) { + added = true; + } + }); + } + return added; + } + const prefix = data.prefix; + if (!validateIconName({ + prefix, + name: "a" + })) { + return false; + } + const storage = getStorage(provider, prefix); + return !!addIconSet(storage, data); +} +function iconLoaded(name) { + return !!getIconData(name); +} +function getIcon(name) { + const result = getIconData(name); + return result ? { + ...defaultIconProps, + ...result + } : result; +} + +const defaultIconSizeCustomisations = Object.freeze({ + width: null, + height: null +}); +const defaultIconCustomisations = Object.freeze({ + // Dimensions + ...defaultIconSizeCustomisations, + // Transformations + ...defaultIconTransformations +}); + +const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; +const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; +function calculateSize(size, ratio, precision) { + if (ratio === 1) { + return size; + } + precision = precision || 100; + if (typeof size === "number") { + return Math.ceil(size * ratio * precision) / precision; + } + if (typeof size !== "string") { + return size; + } + const oldParts = size.split(unitsSplit); + if (oldParts === null || !oldParts.length) { + return size; + } + const newParts = []; + let code = oldParts.shift(); + let isNumber = unitsTest.test(code); + while (true) { + if (isNumber) { + const num = parseFloat(code); + if (isNaN(num)) { + newParts.push(code); + } else { + newParts.push(Math.ceil(num * ratio * precision) / precision); + } + } else { + newParts.push(code); + } + code = oldParts.shift(); + if (code === void 0) { + return newParts.join(""); + } + isNumber = !isNumber; + } +} + +function splitSVGDefs(content, tag = "defs") { + let defs = ""; + const index = content.indexOf("<" + tag); + while (index >= 0) { + const start = content.indexOf(">", index); + const end = content.indexOf("", end); + if (endEnd === -1) { + break; + } + defs += content.slice(start + 1, end).trim(); + content = content.slice(0, index).trim() + content.slice(endEnd + 1); + } + return { + defs, + content + }; +} +function mergeDefsAndContent(defs, content) { + return defs ? "" + defs + "" + content : content; +} +function wrapSVGContent(body, start, end) { + const split = splitSVGDefs(body); + return mergeDefsAndContent(split.defs, start + split.content + end); +} + +const isUnsetKeyword = (value) => value === "unset" || value === "undefined" || value === "none"; +function iconToSVG(icon, customisations) { + const fullIcon = { + ...defaultIconProps, + ...icon + }; + const fullCustomisations = { + ...defaultIconCustomisations, + ...customisations + }; + const box = { + left: fullIcon.left, + top: fullIcon.top, + width: fullIcon.width, + height: fullIcon.height + }; + let body = fullIcon.body; + [fullIcon, fullCustomisations].forEach((props) => { + const transformations = []; + const hFlip = props.hFlip; + const vFlip = props.vFlip; + let rotation = props.rotate; + if (hFlip) { + if (vFlip) { + rotation += 2; + } else { + transformations.push( + "translate(" + (box.width + box.left).toString() + " " + (0 - box.top).toString() + ")" + ); + transformations.push("scale(-1 1)"); + box.top = box.left = 0; + } + } else if (vFlip) { + transformations.push( + "translate(" + (0 - box.left).toString() + " " + (box.height + box.top).toString() + ")" + ); + transformations.push("scale(1 -1)"); + box.top = box.left = 0; + } + let tempValue; + if (rotation < 0) { + rotation -= Math.floor(rotation / 4) * 4; + } + rotation = rotation % 4; + switch (rotation) { + case 1: + tempValue = box.height / 2 + box.top; + transformations.unshift( + "rotate(90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + case 2: + transformations.unshift( + "rotate(180 " + (box.width / 2 + box.left).toString() + " " + (box.height / 2 + box.top).toString() + ")" + ); + break; + case 3: + tempValue = box.width / 2 + box.left; + transformations.unshift( + "rotate(-90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + } + if (rotation % 2 === 1) { + if (box.left !== box.top) { + tempValue = box.left; + box.left = box.top; + box.top = tempValue; + } + if (box.width !== box.height) { + tempValue = box.width; + box.width = box.height; + box.height = tempValue; + } + } + if (transformations.length) { + body = wrapSVGContent( + body, + '', + "" + ); + } + }); + const customisationsWidth = fullCustomisations.width; + const customisationsHeight = fullCustomisations.height; + const boxWidth = box.width; + const boxHeight = box.height; + let width; + let height; + if (customisationsWidth === null) { + height = customisationsHeight === null ? "1em" : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + width = calculateSize(height, boxWidth / boxHeight); + } else { + width = customisationsWidth === "auto" ? boxWidth : customisationsWidth; + height = customisationsHeight === null ? calculateSize(width, boxHeight / boxWidth) : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + } + const attributes = {}; + const setAttr = (prop, value) => { + if (!isUnsetKeyword(value)) { + attributes[prop] = value.toString(); + } + }; + setAttr("width", width); + setAttr("height", height); + const viewBox = [box.left, box.top, boxWidth, boxHeight]; + attributes.viewBox = viewBox.join(" "); + return { + attributes, + viewBox, + body + }; +} + +const regex = /\sid="(\S+)"/g; +const randomPrefix = "IconifyId" + Date.now().toString(16) + (Math.random() * 16777216 | 0).toString(16); +let counter = 0; +function replaceIDs(body, prefix = randomPrefix) { + const ids = []; + let match; + while (match = regex.exec(body)) { + ids.push(match[1]); + } + if (!ids.length) { + return body; + } + const suffix = "suffix" + (Math.random() * 16777216 | Date.now()).toString(16); + ids.forEach((id) => { + const newID = typeof prefix === "function" ? prefix(id) : prefix + (counter++).toString(); + const escapedID = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + body = body.replace( + // Allowed characters before id: [#;"] + // Allowed characters after id: [)"], .[a-z] + new RegExp('([#;"])(' + escapedID + ')([")]|\\.[a-z])', "g"), + "$1" + newID + suffix + "$3" + ); + }); + body = body.replace(new RegExp(suffix, "g"), ""); + return body; +} + +const storage = /* @__PURE__ */ Object.create(null); +function setAPIModule(provider, item) { + storage[provider] = item; +} +function getAPIModule(provider) { + return storage[provider] || storage[""]; +} + +function createAPIConfig(source) { + let resources; + if (typeof source.resources === "string") { + resources = [source.resources]; + } else { + resources = source.resources; + if (!(resources instanceof Array) || !resources.length) { + return null; + } + } + const result = { + // API hosts + resources, + // Root path + path: source.path || "/", + // URL length limit + maxURL: source.maxURL || 500, + // Timeout before next host is used. + rotate: source.rotate || 750, + // Timeout before failing query. + timeout: source.timeout || 5e3, + // Randomise default API end point. + random: source.random === true, + // Start index + index: source.index || 0, + // Receive data after time out (used if time out kicks in first, then API module sends data anyway). + dataAfterTimeout: source.dataAfterTimeout !== false + }; + return result; +} +const configStorage = /* @__PURE__ */ Object.create(null); +const fallBackAPISources = [ + "https://api.simplesvg.com", + "https://api.unisvg.com" +]; +const fallBackAPI = []; +while (fallBackAPISources.length > 0) { + if (fallBackAPISources.length === 1) { + fallBackAPI.push(fallBackAPISources.shift()); + } else { + if (Math.random() > 0.5) { + fallBackAPI.push(fallBackAPISources.shift()); + } else { + fallBackAPI.push(fallBackAPISources.pop()); + } + } +} +configStorage[""] = createAPIConfig({ + resources: ["https://api.iconify.design"].concat(fallBackAPI) +}); +function addAPIProvider(provider, customConfig) { + const config = createAPIConfig(customConfig); + if (config === null) { + return false; + } + configStorage[provider] = config; + return true; +} +function getAPIConfig(provider) { + return configStorage[provider]; +} +function listAPIProviders() { + return Object.keys(configStorage); +} + +const detectFetch = () => { + let callback; + try { + callback = fetch; + if (typeof callback === "function") { + return callback; + } + } catch (err) { + } +}; +let fetchModule = detectFetch(); +function setFetch(fetch2) { + fetchModule = fetch2; +} +function getFetch() { + return fetchModule; +} +function calculateMaxLength(provider, prefix) { + const config = getAPIConfig(provider); + if (!config) { + return 0; + } + let result; + if (!config.maxURL) { + result = 0; + } else { + let maxHostLength = 0; + config.resources.forEach((item) => { + const host = item; + maxHostLength = Math.max(maxHostLength, host.length); + }); + const url = prefix + ".json?icons="; + result = config.maxURL - maxHostLength - config.path.length - url.length; + } + return result; +} +function shouldAbort(status) { + return status === 404; +} +const prepare = (provider, prefix, icons) => { + const results = []; + const maxLength = calculateMaxLength(provider, prefix); + const type = "icons"; + let item = { + type, + provider, + prefix, + icons: [] + }; + let length = 0; + icons.forEach((name, index) => { + length += name.length + 1; + if (length >= maxLength && index > 0) { + results.push(item); + item = { + type, + provider, + prefix, + icons: [] + }; + length = name.length; + } + item.icons.push(name); + }); + results.push(item); + return results; +}; +function getPath(provider) { + if (typeof provider === "string") { + const config = getAPIConfig(provider); + if (config) { + return config.path; + } + } + return "/"; +} +const send = (host, params, callback) => { + if (!fetchModule) { + callback("abort", 424); + return; + } + let path = getPath(params.provider); + switch (params.type) { + case "icons": { + const prefix = params.prefix; + const icons = params.icons; + const iconsList = icons.join(","); + const urlParams = new URLSearchParams({ + icons: iconsList + }); + path += prefix + ".json?" + urlParams.toString(); + break; + } + case "custom": { + const uri = params.uri; + path += uri.slice(0, 1) === "/" ? uri.slice(1) : uri; + break; + } + default: + callback("abort", 400); + return; + } + let defaultError = 503; + fetchModule(host + path).then((response) => { + const status = response.status; + if (status !== 200) { + setTimeout(() => { + callback(shouldAbort(status) ? "abort" : "next", status); + }); + return; + } + defaultError = 501; + return response.json(); + }).then((data) => { + if (typeof data !== "object" || data === null) { + setTimeout(() => { + if (data === 404) { + callback("abort", data); + } else { + callback("next", defaultError); + } + }); + return; + } + setTimeout(() => { + callback("success", data); + }); + }).catch(() => { + callback("next", defaultError); + }); +}; +const fetchAPIModule = { + prepare, + send +}; + +function sortIcons(icons) { + const result = { + loaded: [], + missing: [], + pending: [] + }; + const storage = /* @__PURE__ */ Object.create(null); + icons.sort((a, b) => { + if (a.provider !== b.provider) { + return a.provider.localeCompare(b.provider); + } + if (a.prefix !== b.prefix) { + return a.prefix.localeCompare(b.prefix); + } + return a.name.localeCompare(b.name); + }); + let lastIcon = { + provider: "", + prefix: "", + name: "" + }; + icons.forEach((icon) => { + if (lastIcon.name === icon.name && lastIcon.prefix === icon.prefix && lastIcon.provider === icon.provider) { + return; + } + lastIcon = icon; + const provider = icon.provider; + const prefix = icon.prefix; + const name = icon.name; + const providerStorage = storage[provider] || (storage[provider] = /* @__PURE__ */ Object.create(null)); + const localStorage = providerStorage[prefix] || (providerStorage[prefix] = getStorage(provider, prefix)); + let list; + if (name in localStorage.icons) { + list = result.loaded; + } else if (prefix === "" || localStorage.missing.has(name)) { + list = result.missing; + } else { + list = result.pending; + } + const item = { + provider, + prefix, + name + }; + list.push(item); + }); + return result; +} + +function removeCallback(storages, id) { + storages.forEach((storage) => { + const items = storage.loaderCallbacks; + if (items) { + storage.loaderCallbacks = items.filter((row) => row.id !== id); + } + }); +} +function updateCallbacks(storage) { + if (!storage.pendingCallbacksFlag) { + storage.pendingCallbacksFlag = true; + setTimeout(() => { + storage.pendingCallbacksFlag = false; + const items = storage.loaderCallbacks ? storage.loaderCallbacks.slice(0) : []; + if (!items.length) { + return; + } + let hasPending = false; + const provider = storage.provider; + const prefix = storage.prefix; + items.forEach((item) => { + const icons = item.icons; + const oldLength = icons.pending.length; + icons.pending = icons.pending.filter((icon) => { + if (icon.prefix !== prefix) { + return true; + } + const name = icon.name; + if (storage.icons[name]) { + icons.loaded.push({ + provider, + prefix, + name + }); + } else if (storage.missing.has(name)) { + icons.missing.push({ + provider, + prefix, + name + }); + } else { + hasPending = true; + return true; + } + return false; + }); + if (icons.pending.length !== oldLength) { + if (!hasPending) { + removeCallback([storage], item.id); + } + item.callback( + icons.loaded.slice(0), + icons.missing.slice(0), + icons.pending.slice(0), + item.abort + ); + } + }); + }); + } +} +let idCounter = 0; +function storeCallback(callback, icons, pendingSources) { + const id = idCounter++; + const abort = removeCallback.bind(null, pendingSources, id); + if (!icons.pending.length) { + return abort; + } + const item = { + id, + icons, + callback, + abort + }; + pendingSources.forEach((storage) => { + (storage.loaderCallbacks || (storage.loaderCallbacks = [])).push(item); + }); + return abort; +} + +function listToIcons(list, validate = true, simpleNames = false) { + const result = []; + list.forEach((item) => { + const icon = typeof item === "string" ? stringToIcon(item, validate, simpleNames) : item; + if (icon) { + result.push(icon); + } + }); + return result; +} + +// src/config.ts +var defaultConfig = { + resources: [], + index: 0, + timeout: 2e3, + rotate: 750, + random: false, + dataAfterTimeout: false +}; + +// src/query.ts +function sendQuery(config, payload, query, done) { + const resourcesCount = config.resources.length; + const startIndex = config.random ? Math.floor(Math.random() * resourcesCount) : config.index; + let resources; + if (config.random) { + let list = config.resources.slice(0); + resources = []; + while (list.length > 1) { + const nextIndex = Math.floor(Math.random() * list.length); + resources.push(list[nextIndex]); + list = list.slice(0, nextIndex).concat(list.slice(nextIndex + 1)); + } + resources = resources.concat(list); + } else { + resources = config.resources.slice(startIndex).concat(config.resources.slice(0, startIndex)); + } + const startTime = Date.now(); + let status = "pending"; + let queriesSent = 0; + let lastError; + let timer = null; + let queue = []; + let doneCallbacks = []; + if (typeof done === "function") { + doneCallbacks.push(done); + } + function resetTimer() { + if (timer) { + clearTimeout(timer); + timer = null; + } + } + function abort() { + if (status === "pending") { + status = "aborted"; + } + resetTimer(); + queue.forEach((item) => { + if (item.status === "pending") { + item.status = "aborted"; + } + }); + queue = []; + } + function subscribe(callback, overwrite) { + if (overwrite) { + doneCallbacks = []; + } + if (typeof callback === "function") { + doneCallbacks.push(callback); + } + } + function getQueryStatus() { + return { + startTime, + payload, + status, + queriesSent, + queriesPending: queue.length, + subscribe, + abort + }; + } + function failQuery() { + status = "failed"; + doneCallbacks.forEach((callback) => { + callback(void 0, lastError); + }); + } + function clearQueue() { + queue.forEach((item) => { + if (item.status === "pending") { + item.status = "aborted"; + } + }); + queue = []; + } + function moduleResponse(item, response, data) { + const isError = response !== "success"; + queue = queue.filter((queued) => queued !== item); + switch (status) { + case "pending": + break; + case "failed": + if (isError || !config.dataAfterTimeout) { + return; + } + break; + default: + return; + } + if (response === "abort") { + lastError = data; + failQuery(); + return; + } + if (isError) { + lastError = data; + if (!queue.length) { + if (!resources.length) { + failQuery(); + } else { + execNext(); + } + } + return; + } + resetTimer(); + clearQueue(); + if (!config.random) { + const index = config.resources.indexOf(item.resource); + if (index !== -1 && index !== config.index) { + config.index = index; + } + } + status = "completed"; + doneCallbacks.forEach((callback) => { + callback(data); + }); + } + function execNext() { + if (status !== "pending") { + return; + } + resetTimer(); + const resource = resources.shift(); + if (resource === void 0) { + if (queue.length) { + timer = setTimeout(() => { + resetTimer(); + if (status === "pending") { + clearQueue(); + failQuery(); + } + }, config.timeout); + return; + } + failQuery(); + return; + } + const item = { + status: "pending", + resource, + callback: (status2, data) => { + moduleResponse(item, status2, data); + } + }; + queue.push(item); + queriesSent++; + timer = setTimeout(execNext, config.rotate); + query(resource, payload, item.callback); + } + setTimeout(execNext); + return getQueryStatus; +} + +// src/index.ts +function initRedundancy(cfg) { + const config = { + ...defaultConfig, + ...cfg + }; + let queries = []; + function cleanup() { + queries = queries.filter((item) => item().status === "pending"); + } + function query(payload, queryCallback, doneCallback) { + const query2 = sendQuery( + config, + payload, + queryCallback, + (data, error) => { + cleanup(); + if (doneCallback) { + doneCallback(data, error); + } + } + ); + queries.push(query2); + return query2; + } + function find(callback) { + return queries.find((value) => { + return callback(value); + }) || null; + } + const instance = { + query, + find, + setIndex: (index) => { + config.index = index; + }, + getIndex: () => config.index, + cleanup + }; + return instance; +} + +function emptyCallback$1() { +} +const redundancyCache = /* @__PURE__ */ Object.create(null); +function getRedundancyCache(provider) { + if (!redundancyCache[provider]) { + const config = getAPIConfig(provider); + if (!config) { + return; + } + const redundancy = initRedundancy(config); + const cachedReundancy = { + config, + redundancy + }; + redundancyCache[provider] = cachedReundancy; + } + return redundancyCache[provider]; +} +function sendAPIQuery(target, query, callback) { + let redundancy; + let send; + if (typeof target === "string") { + const api = getAPIModule(target); + if (!api) { + callback(void 0, 424); + return emptyCallback$1; + } + send = api.send; + const cached = getRedundancyCache(target); + if (cached) { + redundancy = cached.redundancy; + } + } else { + const config = createAPIConfig(target); + if (config) { + redundancy = initRedundancy(config); + const moduleKey = target.resources ? target.resources[0] : ""; + const api = getAPIModule(moduleKey); + if (api) { + send = api.send; + } + } + } + if (!redundancy || !send) { + callback(void 0, 424); + return emptyCallback$1; + } + return redundancy.query(query, send, callback)().abort; +} + +function emptyCallback() { +} +function loadedNewIcons(storage) { + if (!storage.iconsLoaderFlag) { + storage.iconsLoaderFlag = true; + setTimeout(() => { + storage.iconsLoaderFlag = false; + updateCallbacks(storage); + }); + } +} +function checkIconNamesForAPI(icons) { + const valid = []; + const invalid = []; + icons.forEach((name) => { + (name.match(matchIconName) ? valid : invalid).push(name); + }); + return { + valid, + invalid + }; +} +function parseLoaderResponse(storage, icons, data) { + function checkMissing() { + const pending = storage.pendingIcons; + icons.forEach((name) => { + if (pending) { + pending.delete(name); + } + if (!storage.icons[name]) { + storage.missing.add(name); + } + }); + } + if (data && typeof data === "object") { + try { + const parsed = addIconSet(storage, data); + if (!parsed.length) { + checkMissing(); + return; + } + } catch (err) { + console.error(err); + } + } + checkMissing(); + loadedNewIcons(storage); +} +function parsePossiblyAsyncResponse(response, callback) { + if (response instanceof Promise) { + response.then((data) => { + callback(data); + }).catch(() => { + callback(null); + }); + } else { + callback(response); + } +} +function loadNewIcons(storage, icons) { + if (!storage.iconsToLoad) { + storage.iconsToLoad = icons; + } else { + storage.iconsToLoad = storage.iconsToLoad.concat(icons).sort(); + } + if (!storage.iconsQueueFlag) { + storage.iconsQueueFlag = true; + setTimeout(() => { + storage.iconsQueueFlag = false; + const { provider, prefix } = storage; + const icons2 = storage.iconsToLoad; + delete storage.iconsToLoad; + if (!icons2 || !icons2.length) { + return; + } + const customIconLoader = storage.loadIcon; + if (storage.loadIcons && (icons2.length > 1 || !customIconLoader)) { + parsePossiblyAsyncResponse( + storage.loadIcons(icons2, prefix, provider), + (data) => { + parseLoaderResponse(storage, icons2, data); + } + ); + return; + } + if (customIconLoader) { + icons2.forEach((name) => { + const response = customIconLoader(name, prefix, provider); + parsePossiblyAsyncResponse(response, (data) => { + const iconSet = data ? { + prefix, + icons: { + [name]: data + } + } : null; + parseLoaderResponse(storage, [name], iconSet); + }); + }); + return; + } + const { valid, invalid } = checkIconNamesForAPI(icons2); + if (invalid.length) { + parseLoaderResponse(storage, invalid, null); + } + if (!valid.length) { + return; + } + const api = prefix.match(matchIconName) ? getAPIModule(provider) : null; + if (!api) { + parseLoaderResponse(storage, valid, null); + return; + } + const params = api.prepare(provider, prefix, valid); + params.forEach((item) => { + sendAPIQuery(provider, item, (data) => { + parseLoaderResponse(storage, item.icons, data); + }); + }); + }); + } +} +const loadIcons = (icons, callback) => { + const cleanedIcons = listToIcons(icons, true, allowSimpleNames()); + const sortedIcons = sortIcons(cleanedIcons); + if (!sortedIcons.pending.length) { + let callCallback = true; + if (callback) { + setTimeout(() => { + if (callCallback) { + callback( + sortedIcons.loaded, + sortedIcons.missing, + sortedIcons.pending, + emptyCallback + ); + } + }); + } + return () => { + callCallback = false; + }; + } + const newIcons = /* @__PURE__ */ Object.create(null); + const sources = []; + let lastProvider, lastPrefix; + sortedIcons.pending.forEach((icon) => { + const { provider, prefix } = icon; + if (prefix === lastPrefix && provider === lastProvider) { + return; + } + lastProvider = provider; + lastPrefix = prefix; + sources.push(getStorage(provider, prefix)); + const providerNewIcons = newIcons[provider] || (newIcons[provider] = /* @__PURE__ */ Object.create(null)); + if (!providerNewIcons[prefix]) { + providerNewIcons[prefix] = []; + } + }); + sortedIcons.pending.forEach((icon) => { + const { provider, prefix, name } = icon; + const storage = getStorage(provider, prefix); + const pendingQueue = storage.pendingIcons || (storage.pendingIcons = /* @__PURE__ */ new Set()); + if (!pendingQueue.has(name)) { + pendingQueue.add(name); + newIcons[provider][prefix].push(name); + } + }); + sources.forEach((storage) => { + const list = newIcons[storage.provider][storage.prefix]; + if (list.length) { + loadNewIcons(storage, list); + } + }); + return callback ? storeCallback(callback, sortedIcons, sources) : emptyCallback; +}; +const loadIcon = (icon) => { + return new Promise((fulfill, reject) => { + const iconObj = typeof icon === "string" ? stringToIcon(icon, true) : icon; + if (!iconObj) { + reject(icon); + return; + } + loadIcons([iconObj || icon], (loaded) => { + if (loaded.length && iconObj) { + const data = getIconData(iconObj); + if (data) { + fulfill({ + ...defaultIconProps, + ...data + }); + return; + } + } + reject(icon); + }); + }); +}; + +function setCustomIconsLoader(loader, prefix, provider) { + getStorage(provider || "", prefix).loadIcons = loader; +} +function setCustomIconLoader(loader, prefix, provider) { + getStorage(provider || "", prefix).loadIcon = loader; +} + +function mergeCustomisations(defaults, item) { + const result = { + ...defaults + }; + for (const key in item) { + const value = item[key]; + const valueType = typeof value; + if (key in defaultIconSizeCustomisations) { + if (value === null || value && (valueType === "string" || valueType === "number")) { + result[key] = value; + } + } else if (valueType === typeof result[key]) { + result[key] = key === "rotate" ? value % 4 : value; + } + } + return result; +} + +const separator = /[\s,]+/; +function flipFromString(custom, flip) { + flip.split(separator).forEach((str) => { + const value = str.trim(); + switch (value) { + case "horizontal": + custom.hFlip = true; + break; + case "vertical": + custom.vFlip = true; + break; + } + }); +} + +function rotateFromString(value, defaultValue = 0) { + const units = value.replace(/^-?[0-9.]*/, ""); + function cleanup(value2) { + while (value2 < 0) { + value2 += 4; + } + return value2 % 4; + } + if (units === "") { + const num = parseInt(value); + return isNaN(num) ? 0 : cleanup(num); + } else if (units !== value) { + let split = 0; + switch (units) { + case "%": + split = 25; + break; + case "deg": + split = 90; + } + if (split) { + let num = parseFloat(value.slice(0, value.length - units.length)); + if (isNaN(num)) { + return 0; + } + num = num / split; + return num % 1 === 0 ? cleanup(num) : 0; + } + } + return defaultValue; +} + +function iconToHTML(body, attributes) { + let renderAttribsHTML = body.indexOf("xlink:") === -1 ? "" : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; + for (const attr in attributes) { + renderAttribsHTML += " " + attr + '="' + attributes[attr] + '"'; + } + return '" + body + ""; +} + +function encodeSVGforURL(svg) { + return svg.replace(/"/g, "'").replace(/%/g, "%25").replace(/#/g, "%23").replace(//g, "%3E").replace(/\s+/g, " "); +} +function svgToData(svg) { + return "data:image/svg+xml," + encodeSVGforURL(svg); +} +function svgToURL(svg) { + return 'url("' + svgToData(svg) + '")'; +} + +let policy; +function createPolicy() { + try { + policy = window.trustedTypes.createPolicy("iconify", { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + createHTML: (s) => s + }); + } catch (err) { + policy = null; + } +} +function cleanUpInnerHTML(html) { + if (policy === void 0) { + createPolicy(); + } + return policy ? policy.createHTML(html) : html; +} + +const defaultExtendedIconCustomisations = { + ...defaultIconCustomisations, + inline: false, +}; + +/** + * Default SVG attributes + */ +const svgDefaults = { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', +}; +/** + * Style modes + */ +const commonProps = { + display: 'inline-block', +}; +const monotoneProps = { + backgroundColor: 'currentColor', +}; +const coloredProps = { + backgroundColor: 'transparent', +}; +// Dynamically add common props to variables above +const propsToAdd = { + Image: 'var(--svg)', + Repeat: 'no-repeat', + Size: '100% 100%', +}; +const propsToAddTo = { + WebkitMask: monotoneProps, + mask: monotoneProps, + background: coloredProps, +}; +for (const prefix in propsToAddTo) { + const list = propsToAddTo[prefix]; + for (const prop in propsToAdd) { + list[prefix + prop] = propsToAdd[prop]; + } +} +/** + * Default values for customisations for inline icon + */ +const inlineDefaults = { + ...defaultExtendedIconCustomisations, + inline: true, +}; +/** + * Fix size: add 'px' to numbers + */ +function fixSize(value) { + return value + (value.match(/^[-0-9.]+$/) ? 'px' : ''); +} +/** + * Render icon + */ +const render = ( +// Icon must be validated before calling this function +icon, +// Partial properties +props, +// Icon name +name) => { + // Get default properties + const defaultProps = props.inline + ? inlineDefaults + : defaultExtendedIconCustomisations; + // Get all customisations + const customisations = mergeCustomisations(defaultProps, props); + // Check mode + const mode = props.mode || 'svg'; + // Create style + const style = {}; + const customStyle = props.style || {}; + // Create SVG component properties + const componentProps = { + ...(mode === 'svg' ? svgDefaults : {}), + }; + if (name) { + const iconName = stringToIcon(name, false, true); + if (iconName) { + const classNames = ['iconify']; + const props = [ + 'provider', + 'prefix', + ]; + for (const prop of props) { + if (iconName[prop]) { + classNames.push('iconify--' + iconName[prop]); + } + } + componentProps.className = classNames.join(' '); + } + } + // Get element properties + for (let key in props) { + const value = props[key]; + if (value === void 0) { + continue; + } + switch (key) { + // Properties to ignore + case 'icon': + case 'style': + case 'children': + case 'onLoad': + case 'mode': + case 'ssr': + break; + // Forward ref + case '_ref': + componentProps.ref = value; + break; + // Merge class names + case 'className': + componentProps[key] = + (componentProps[key] ? componentProps[key] + ' ' : '') + + value; + break; + // Boolean attributes + case 'inline': + case 'hFlip': + case 'vFlip': + customisations[key] = + value === true || value === 'true' || value === 1; + break; + // Flip as string: 'horizontal,vertical' + case 'flip': + if (typeof value === 'string') { + flipFromString(customisations, value); + } + break; + // Color: copy to style + case 'color': + style.color = value; + break; + // Rotation as string + case 'rotate': + if (typeof value === 'string') { + customisations[key] = rotateFromString(value); + } + else if (typeof value === 'number') { + customisations[key] = value; + } + break; + // Remove aria-hidden + case 'ariaHidden': + case 'aria-hidden': + if (value !== true && value !== 'true') { + delete componentProps['aria-hidden']; + } + break; + // Copy missing property if it does not exist in customisations + default: + if (defaultProps[key] === void 0) { + componentProps[key] = value; + } + } + } + // Generate icon + const item = iconToSVG(icon, customisations); + const renderAttribs = item.attributes; + // Inline display + if (customisations.inline) { + style.verticalAlign = '-0.125em'; + } + if (mode === 'svg') { + // Add style + componentProps.style = { + ...style, + ...customStyle, + }; + // Add icon stuff + Object.assign(componentProps, renderAttribs); + // Counter for ids based on "id" property to render icons consistently on server and client + let localCounter = 0; + let id = props.id; + if (typeof id === 'string') { + // Convert '-' to '_' to avoid errors in animations + id = id.replace(/-/g, '_'); + } + // Add icon stuff + componentProps.dangerouslySetInnerHTML = { + __html: cleanUpInnerHTML(replaceIDs(item.body, id ? () => id + 'ID' + localCounter++ : 'iconifyReact')), + }; + return react.createElement('svg', componentProps); + } + // Render with style + const { body, width, height } = icon; + const useMask = mode === 'mask' || + (mode === 'bg' ? false : body.indexOf('currentColor') !== -1); + // Generate SVG + const html = iconToHTML(body, { + ...renderAttribs, + width: width + '', + height: height + '', + }); + // Generate style + componentProps.style = { + ...style, + '--svg': svgToURL(html), + 'width': fixSize(renderAttribs.width), + 'height': fixSize(renderAttribs.height), + ...commonProps, + ...(useMask ? monotoneProps : coloredProps), + ...customStyle, + }; + return react.createElement('span', componentProps); +}; + +/** + * Initialise stuff + */ +// Enable short names +allowSimpleNames(true); +// Set API module +setAPIModule('', fetchAPIModule); +/** + * Browser stuff + */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + const _window = window; + // Load icons from global "IconifyPreload" + if (_window.IconifyPreload !== void 0) { + const preload = _window.IconifyPreload; + const err = 'Invalid IconifyPreload syntax.'; + if (typeof preload === 'object' && preload !== null) { + (preload instanceof Array ? preload : [preload]).forEach((item) => { + try { + if ( + // Check if item is an object and not null/array + typeof item !== 'object' || + item === null || + item instanceof Array || + // Check for 'icons' and 'prefix' + typeof item.icons !== 'object' || + typeof item.prefix !== 'string' || + // Add icon set + !addCollection(item)) { + console.error(err); + } + } + catch (e) { + console.error(err); + } + }); + } + } + // Set API from global "IconifyProviders" + if (_window.IconifyProviders !== void 0) { + const providers = _window.IconifyProviders; + if (typeof providers === 'object' && providers !== null) { + for (let key in providers) { + const err = 'IconifyProviders[' + key + '] is invalid.'; + try { + const value = providers[key]; + if (typeof value !== 'object' || + !value || + value.resources === void 0) { + continue; + } + if (!addAPIProvider(key, value)) { + console.error(err); + } + } + catch (e) { + console.error(err); + } + } + } + } +} +function IconComponent(props) { + const [mounted, setMounted] = react.useState(!!props.ssr); + const [abort, setAbort] = react.useState({}); + // Get initial state + function getInitialState(mounted) { + if (mounted) { + const name = props.icon; + if (typeof name === 'object') { + // Icon as object + return { + name: '', + data: name, + }; + } + const data = getIconData(name); + if (data) { + return { + name, + data, + }; + } + } + return { + name: '', + }; + } + const [state, setState] = react.useState(getInitialState(!!props.ssr)); + // Cancel loading + function cleanup() { + const callback = abort.callback; + if (callback) { + callback(); + setAbort({}); + } + } + // Change state if it is different + function changeState(newState) { + if (JSON.stringify(state) !== JSON.stringify(newState)) { + cleanup(); + setState(newState); + return true; + } + } + // Update state + function updateState() { + var _a; + const name = props.icon; + if (typeof name === 'object') { + // Icon as object + changeState({ + name: '', + data: name, + }); + return; + } + // New icon or got icon data + const data = getIconData(name); + if (changeState({ + name, + data, + })) { + if (data === undefined) { + // Load icon, update state when done + const callback = loadIcons([name], updateState); + setAbort({ + callback, + }); + } + else if (data) { + // Icon data is available: trigger onLoad callback if present + (_a = props.onLoad) === null || _a === void 0 ? void 0 : _a.call(props, name); + } + } + } + // Mounted state, cleanup for loader + react.useEffect(() => { + setMounted(true); + return cleanup; + }, []); + // Icon changed or component mounted + react.useEffect(() => { + if (mounted) { + updateState(); + } + }, [props.icon, mounted]); + // Render icon + const { name, data } = state; + if (!data) { + return props.children + ? props.children + : props.fallback + ? props.fallback + : react.createElement('span', {}); + } + return render({ + ...defaultIconProps, + ...data, + }, props, name); +} +/** + * Block icon + * + * @param props - Component properties + */ +const Icon = react.forwardRef((props, ref) => IconComponent({ + ...props, + _ref: ref, +})); +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +const InlineIcon = react.forwardRef((props, ref) => IconComponent({ + inline: true, + ...props, + _ref: ref, +})); +/** + * Internal API + */ +const _api = { + getAPIConfig, + setAPIModule, + sendAPIQuery, + setFetch, + getFetch, + listAPIProviders, +}; + +exports.Icon = Icon; +exports.InlineIcon = InlineIcon; +exports._api = _api; +exports.addAPIProvider = addAPIProvider; +exports.addCollection = addCollection; +exports.addIcon = addIcon; +exports.buildIcon = iconToSVG; +exports.calculateSize = calculateSize; +exports.getIcon = getIcon; +exports.iconLoaded = iconLoaded; +exports.listIcons = listIcons; +exports.loadIcon = loadIcon; +exports.loadIcons = loadIcons; +exports.replaceIDs = replaceIDs; +exports.setCustomIconLoader = setCustomIconLoader; +exports.setCustomIconsLoader = setCustomIconsLoader; diff --git a/node_modules/@iconify/react/dist/iconify.d.cts b/node_modules/@iconify/react/dist/iconify.d.cts new file mode 100644 index 00000000..5b472ffa --- /dev/null +++ b/node_modules/@iconify/react/dist/iconify.d.cts @@ -0,0 +1,428 @@ +import { IconifyIcon } from '@iconify/types'; +import { IconifyJSON } from '@iconify/types'; +import { IconifyTransformations } from '@iconify/types'; +import type { JSX as JSX_2 } from 'react'; +import type { ReactNode } from 'react'; +import type { SVGProps } from 'react'; + +/** + * Add custom config for provider + */ +export declare function addAPIProvider(provider: string, customConfig: PartialIconifyAPIConfig): boolean; + +/** + * Add icon set + */ +export declare function addCollection(data: IconifyJSON, provider?: string): boolean; + +/** + * Add one icon + */ +export declare function addIcon(name: string, data: IconifyIcon | null): boolean; + +/** + * Internal API + */ +export declare const _api: IconifyAPIInternalFunctions; + +/** + * Get SVG attributes and content from icon + customisations + * + * Does not generate style to make it compatible with frameworks that use objects for style, such as React. + * Instead, it generates 'inline' value. If true, rendering engine should add verticalAlign: -0.125em to icon. + * + * Customisations should be normalised by platform specific parser. + * Result should be converted to by platform specific parser. + * Use replaceIDs to generate unique IDs for body. + */ +export declare function buildIcon(icon: IconifyIcon, customisations?: IconifyIconCustomisations_2): IconifyIconBuildResult; + +/** + * Calculate second dimension when only 1 dimension is set + */ +export declare function calculateSize(size: string, ratio: number, precision?: number): string; + +export declare function calculateSize(size: number, ratio: number, precision?: number): number; + +export declare function calculateSize(size: string | number, ratio: number, precision?: number): string | number; + +/** + * Signature for getAPIConfig + */ +export declare type GetAPIConfig = (provider: string) => IconifyAPIConfig | undefined; + +/** + * Get full icon + */ +export declare function getIcon(name: string): Required | null | undefined; + +/** + * Block icon + * + * @param props - Component properties + */ +export declare const Icon: IconComponentType; + +declare type IconComponentType = (props: IconProps) => JSX_2.Element; + +/** + * API config + */ +export declare interface IconifyAPIConfig extends RedundancyConfig { + path: string; + maxURL: number; +} + +export declare interface IconifyAPICustomQueryParams { + type: 'custom'; + provider?: string; + uri: string; +} + +/** + * Iconify API functions + */ +export declare interface IconifyAPIFunctions { + /** + * Load icons + */ + loadIcons: (icons: (IconifyIconName | string)[], callback?: IconifyIconLoaderCallback) => IconifyIconLoaderAbort; + /** + * Load one icon, using Promise syntax + */ + loadIcon: (icon: IconifyIconName | string) => Promise>; + /** + * Add API provider + */ + addAPIProvider: (provider: string, customConfig: PartialIconifyAPIConfig) => boolean; + /** + * Set custom loader for multple icons + */ + setCustomIconsLoader: (callback: IconifyCustomIconsLoader, prefix: string, provider?: string) => void; + /** + * Set custom loader for one icon + */ + setCustomIconLoader: (callback: IconifyCustomIconLoader, prefix: string, provider?: string) => void; +} + +/** + * Params for sendQuery() + */ +declare interface IconifyAPIIconsQueryParams { + type: 'icons'; + provider: string; + prefix: string; + icons: string[]; +} + +/** + * Exposed internal functions + * + * Used by plug-ins, such as Icon Finder + * + * Important: any changes published in a release must be backwards compatible. + */ +export declare interface IconifyAPIInternalFunctions { + /** + * Get API config, used by custom modules + */ + getAPIConfig: GetAPIConfig; + /** + * Set custom API module + */ + setAPIModule: (provider: string, item: IconifyAPIModule) => void; + /** + * Send API query + */ + sendAPIQuery: (target: string | PartialIconifyAPIConfig, query: IconifyAPIQueryParams, callback: QueryDoneCallback) => QueryAbortCallback; + /** + * Set and get fetch() + */ + setFetch: (item: typeof fetch) => void; + getFetch: () => typeof fetch | undefined; + /** + * List all API providers (from config) + */ + listAPIProviders: () => string[]; +} + +/** + * API modules + */ +export declare interface IconifyAPIModule { + prepare: IconifyAPIPrepareIconsQuery; + send: IconifyAPISendQuery; +} + +/** + * Functions to implement in module + */ +export declare type IconifyAPIPrepareIconsQuery = (provider: string, prefix: string, icons: string[]) => IconifyAPIIconsQueryParams[]; + +export declare type IconifyAPIQueryParams = IconifyAPIIconsQueryParams | IconifyAPICustomQueryParams; + +export declare type IconifyAPISendQuery = (host: string, params: IconifyAPIQueryParams, callback: QueryModuleResponse) => void; + +/** + * Interface for exported builder functions + */ +export declare interface IconifyBuilderFunctions { + replaceIDs?: (body: string, prefix?: string | (() => string)) => string; + calculateSize: (size: string | number, ratio: number, precision?: number) => string | number; + buildIcon: (icon: IconifyIcon, customisations?: IconifyIconCustomisations_2) => IconifyIconBuildResult; +} + +/** + * Custom loader for one icon + */ +export declare type IconifyCustomIconLoader = (name: string, prefix: string, provider: string) => Promise | IconifyIcon | null; + +/** + * Custom icons loader + */ +export declare type IconifyCustomIconsLoader = (icons: string[], prefix: string, provider: string) => Promise | IconifyJSON | null; + +/** + * React component properties: generic element for Icon component, SVG for generated component + */ +declare type IconifyElementProps = SVGProps; + +export { IconifyIcon } + +/** + * Interface for getSVGData() result + */ +export declare interface IconifyIconBuildResult { + attributes: { + width?: string; + height?: string; + viewBox: string; + }; + viewBox: SVGViewBox; + body: string; +} + +/** + * Icon customisations + */ +export declare type IconifyIconCustomisations = IconifyIconCustomisations_2 & { + rotate?: string | number; + inline?: boolean; +}; + +/** + * Icon customisations + */ +declare interface IconifyIconCustomisations_2 extends IconifyTransformations, IconifyIconSizeCustomisations { +} + +/** + * Function to abort loading (usually just removes callback because loading is already in progress) + */ +export declare type IconifyIconLoaderAbort = () => void; + +/** + * Loader callback + * + * Provides list of icons that have been loaded + */ +export declare type IconifyIconLoaderCallback = (loaded: IconifyIconName[], missing: IconifyIconName[], pending: IconifyIconName[], unsubscribe: IconifyIconLoaderAbort) => void; + +/** + * Icon name + */ +export declare interface IconifyIconName { + readonly provider: string; + readonly prefix: string; + readonly name: string; +} + +/** + * Callback for when icon has been loaded (only triggered for icons loaded from API) + */ +export declare type IconifyIconOnLoad = (name: string) => void; + +/** + * Icon properties + */ +export declare interface IconifyIconProps extends IconifyIconCustomisations { + icon: IconifyIcon | string; + mode?: IconifyRenderMode; + color?: string; + flip?: string; + id?: string; + ssr?: boolean; + fallback?: ReactNode; + onLoad?: IconifyIconOnLoad; +} + +/** + * Icon size + */ +export declare type IconifyIconSize = null | string | number; + +/** + * Dimensions + */ +declare interface IconifyIconSizeCustomisations { + width?: IconifyIconSize; + height?: IconifyIconSize; +} + +export { IconifyJSON } + +/** + * Function to load icons + */ +declare type IconifyLoadIcons = (icons: (IconifyIconName | string)[], callback?: IconifyIconLoaderCallback) => IconifyIconLoaderAbort; + +/** + * Icon render mode + * + * 'style' = 'bg' or 'mask', depending on icon content + * 'bg' = with style using `background` + * 'mask' = with style using `mask` + * 'svg' = + */ +export declare type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'svg'; + +/** + * Interface for exported storage functions + */ +export declare interface IconifyStorageFunctions { + /** + * Check if icon data is available + */ + iconLoaded: (name: string) => boolean; + /** + * Get icon data with all properties + * + * Returns null if icon is missing (attempted to load, but failed) + * Returns undefined if icon was not loaded + */ + getIcon: (name: string) => Required | null | undefined; + /** + * List all available icons + */ + listIcons: (provider?: string, prefix?: string) => string[]; + /** + * Add icon to storage + * + * Data is null if icon is missing + */ + addIcon: (name: string, data: IconifyIcon | null) => boolean; + /** + * Add icon set to storage + */ + addCollection: (data: IconifyJSON, provider?: string) => boolean; +} + +/** + * Check if icon data is available + */ +export declare function iconLoaded(name: string): boolean; + +/** + * Mix of icon properties and SVGSVGElement properties + */ +export declare type IconProps = IconifyElementProps & IconifyIconProps; + +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +export declare const InlineIcon: IconComponentType; + +/** + * List available icons + */ +export declare function listIcons(provider?: string, prefix?: string): string[]; + +/** + * Load one icon using Promise + */ +export declare const loadIcon: (icon: IconifyIconName | string) => Promise>; + +/** + * Load icons + */ +export declare const loadIcons: IconifyLoadIcons; + +export declare type PartialIconifyAPIConfig = Partial & Pick; + +/** + * Callback for "abort" pending item. + */ +declare type QueryAbortCallback = () => void; + +/** + * Callback + * + * If error is present, something went wrong and data is undefined. If error is undefined, data is set. + */ +declare type QueryDoneCallback = (data?: QueryModuleResponseData, error?: QueryModuleResponseData) => void; + +declare type QueryModuleResponse = (status: QueryModuleResponseType, data: QueryModuleResponseData) => void; + +/** + * Response from query module + */ +declare type QueryModuleResponseData = unknown; + +/** + * Response from query module + */ +declare type QueryModuleResponseType = 'success' | 'next' | 'abort'; + +/** + * Configuration object + */ +declare interface RedundancyConfig { + resources: RedundancyResource[]; + index: number; + timeout: number; + rotate: number; + random: boolean; + dataAfterTimeout: boolean; +} + +/** + * Resource to rotate (usually hostname or partial URL) + */ +declare type RedundancyResource = string; + +/** + * IDs usage: + * + * id="{id}" + * xlink:href="#{id}" + * url(#{id}) + * + * From SVG animations: + * + * begin="0;{id}.end" + * begin="{id}.end" + * begin="{id}.click" + */ +/** + * Replace IDs in SVG output with unique IDs + */ +export declare function replaceIDs(body: string, prefix?: string | ((id: string) => string)): string; + +/** + * Set custom loader for one icon + */ +export declare function setCustomIconLoader(loader: IconifyCustomIconLoader, prefix: string, provider?: string): void; + +/** + * Set custom loader for multiple icons + */ +export declare function setCustomIconsLoader(loader: IconifyCustomIconsLoader, prefix: string, provider?: string): void; + +/** + * SVG viewBox: x, y, width, height + */ +declare type SVGViewBox = [x: number, y: number, width: number, height: number]; + +export { } diff --git a/node_modules/@iconify/react/dist/iconify.d.ts b/node_modules/@iconify/react/dist/iconify.d.ts new file mode 100644 index 00000000..5b472ffa --- /dev/null +++ b/node_modules/@iconify/react/dist/iconify.d.ts @@ -0,0 +1,428 @@ +import { IconifyIcon } from '@iconify/types'; +import { IconifyJSON } from '@iconify/types'; +import { IconifyTransformations } from '@iconify/types'; +import type { JSX as JSX_2 } from 'react'; +import type { ReactNode } from 'react'; +import type { SVGProps } from 'react'; + +/** + * Add custom config for provider + */ +export declare function addAPIProvider(provider: string, customConfig: PartialIconifyAPIConfig): boolean; + +/** + * Add icon set + */ +export declare function addCollection(data: IconifyJSON, provider?: string): boolean; + +/** + * Add one icon + */ +export declare function addIcon(name: string, data: IconifyIcon | null): boolean; + +/** + * Internal API + */ +export declare const _api: IconifyAPIInternalFunctions; + +/** + * Get SVG attributes and content from icon + customisations + * + * Does not generate style to make it compatible with frameworks that use objects for style, such as React. + * Instead, it generates 'inline' value. If true, rendering engine should add verticalAlign: -0.125em to icon. + * + * Customisations should be normalised by platform specific parser. + * Result should be converted to by platform specific parser. + * Use replaceIDs to generate unique IDs for body. + */ +export declare function buildIcon(icon: IconifyIcon, customisations?: IconifyIconCustomisations_2): IconifyIconBuildResult; + +/** + * Calculate second dimension when only 1 dimension is set + */ +export declare function calculateSize(size: string, ratio: number, precision?: number): string; + +export declare function calculateSize(size: number, ratio: number, precision?: number): number; + +export declare function calculateSize(size: string | number, ratio: number, precision?: number): string | number; + +/** + * Signature for getAPIConfig + */ +export declare type GetAPIConfig = (provider: string) => IconifyAPIConfig | undefined; + +/** + * Get full icon + */ +export declare function getIcon(name: string): Required | null | undefined; + +/** + * Block icon + * + * @param props - Component properties + */ +export declare const Icon: IconComponentType; + +declare type IconComponentType = (props: IconProps) => JSX_2.Element; + +/** + * API config + */ +export declare interface IconifyAPIConfig extends RedundancyConfig { + path: string; + maxURL: number; +} + +export declare interface IconifyAPICustomQueryParams { + type: 'custom'; + provider?: string; + uri: string; +} + +/** + * Iconify API functions + */ +export declare interface IconifyAPIFunctions { + /** + * Load icons + */ + loadIcons: (icons: (IconifyIconName | string)[], callback?: IconifyIconLoaderCallback) => IconifyIconLoaderAbort; + /** + * Load one icon, using Promise syntax + */ + loadIcon: (icon: IconifyIconName | string) => Promise>; + /** + * Add API provider + */ + addAPIProvider: (provider: string, customConfig: PartialIconifyAPIConfig) => boolean; + /** + * Set custom loader for multple icons + */ + setCustomIconsLoader: (callback: IconifyCustomIconsLoader, prefix: string, provider?: string) => void; + /** + * Set custom loader for one icon + */ + setCustomIconLoader: (callback: IconifyCustomIconLoader, prefix: string, provider?: string) => void; +} + +/** + * Params for sendQuery() + */ +declare interface IconifyAPIIconsQueryParams { + type: 'icons'; + provider: string; + prefix: string; + icons: string[]; +} + +/** + * Exposed internal functions + * + * Used by plug-ins, such as Icon Finder + * + * Important: any changes published in a release must be backwards compatible. + */ +export declare interface IconifyAPIInternalFunctions { + /** + * Get API config, used by custom modules + */ + getAPIConfig: GetAPIConfig; + /** + * Set custom API module + */ + setAPIModule: (provider: string, item: IconifyAPIModule) => void; + /** + * Send API query + */ + sendAPIQuery: (target: string | PartialIconifyAPIConfig, query: IconifyAPIQueryParams, callback: QueryDoneCallback) => QueryAbortCallback; + /** + * Set and get fetch() + */ + setFetch: (item: typeof fetch) => void; + getFetch: () => typeof fetch | undefined; + /** + * List all API providers (from config) + */ + listAPIProviders: () => string[]; +} + +/** + * API modules + */ +export declare interface IconifyAPIModule { + prepare: IconifyAPIPrepareIconsQuery; + send: IconifyAPISendQuery; +} + +/** + * Functions to implement in module + */ +export declare type IconifyAPIPrepareIconsQuery = (provider: string, prefix: string, icons: string[]) => IconifyAPIIconsQueryParams[]; + +export declare type IconifyAPIQueryParams = IconifyAPIIconsQueryParams | IconifyAPICustomQueryParams; + +export declare type IconifyAPISendQuery = (host: string, params: IconifyAPIQueryParams, callback: QueryModuleResponse) => void; + +/** + * Interface for exported builder functions + */ +export declare interface IconifyBuilderFunctions { + replaceIDs?: (body: string, prefix?: string | (() => string)) => string; + calculateSize: (size: string | number, ratio: number, precision?: number) => string | number; + buildIcon: (icon: IconifyIcon, customisations?: IconifyIconCustomisations_2) => IconifyIconBuildResult; +} + +/** + * Custom loader for one icon + */ +export declare type IconifyCustomIconLoader = (name: string, prefix: string, provider: string) => Promise | IconifyIcon | null; + +/** + * Custom icons loader + */ +export declare type IconifyCustomIconsLoader = (icons: string[], prefix: string, provider: string) => Promise | IconifyJSON | null; + +/** + * React component properties: generic element for Icon component, SVG for generated component + */ +declare type IconifyElementProps = SVGProps; + +export { IconifyIcon } + +/** + * Interface for getSVGData() result + */ +export declare interface IconifyIconBuildResult { + attributes: { + width?: string; + height?: string; + viewBox: string; + }; + viewBox: SVGViewBox; + body: string; +} + +/** + * Icon customisations + */ +export declare type IconifyIconCustomisations = IconifyIconCustomisations_2 & { + rotate?: string | number; + inline?: boolean; +}; + +/** + * Icon customisations + */ +declare interface IconifyIconCustomisations_2 extends IconifyTransformations, IconifyIconSizeCustomisations { +} + +/** + * Function to abort loading (usually just removes callback because loading is already in progress) + */ +export declare type IconifyIconLoaderAbort = () => void; + +/** + * Loader callback + * + * Provides list of icons that have been loaded + */ +export declare type IconifyIconLoaderCallback = (loaded: IconifyIconName[], missing: IconifyIconName[], pending: IconifyIconName[], unsubscribe: IconifyIconLoaderAbort) => void; + +/** + * Icon name + */ +export declare interface IconifyIconName { + readonly provider: string; + readonly prefix: string; + readonly name: string; +} + +/** + * Callback for when icon has been loaded (only triggered for icons loaded from API) + */ +export declare type IconifyIconOnLoad = (name: string) => void; + +/** + * Icon properties + */ +export declare interface IconifyIconProps extends IconifyIconCustomisations { + icon: IconifyIcon | string; + mode?: IconifyRenderMode; + color?: string; + flip?: string; + id?: string; + ssr?: boolean; + fallback?: ReactNode; + onLoad?: IconifyIconOnLoad; +} + +/** + * Icon size + */ +export declare type IconifyIconSize = null | string | number; + +/** + * Dimensions + */ +declare interface IconifyIconSizeCustomisations { + width?: IconifyIconSize; + height?: IconifyIconSize; +} + +export { IconifyJSON } + +/** + * Function to load icons + */ +declare type IconifyLoadIcons = (icons: (IconifyIconName | string)[], callback?: IconifyIconLoaderCallback) => IconifyIconLoaderAbort; + +/** + * Icon render mode + * + * 'style' = 'bg' or 'mask', depending on icon content + * 'bg' = with style using `background` + * 'mask' = with style using `mask` + * 'svg' = + */ +export declare type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'svg'; + +/** + * Interface for exported storage functions + */ +export declare interface IconifyStorageFunctions { + /** + * Check if icon data is available + */ + iconLoaded: (name: string) => boolean; + /** + * Get icon data with all properties + * + * Returns null if icon is missing (attempted to load, but failed) + * Returns undefined if icon was not loaded + */ + getIcon: (name: string) => Required | null | undefined; + /** + * List all available icons + */ + listIcons: (provider?: string, prefix?: string) => string[]; + /** + * Add icon to storage + * + * Data is null if icon is missing + */ + addIcon: (name: string, data: IconifyIcon | null) => boolean; + /** + * Add icon set to storage + */ + addCollection: (data: IconifyJSON, provider?: string) => boolean; +} + +/** + * Check if icon data is available + */ +export declare function iconLoaded(name: string): boolean; + +/** + * Mix of icon properties and SVGSVGElement properties + */ +export declare type IconProps = IconifyElementProps & IconifyIconProps; + +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +export declare const InlineIcon: IconComponentType; + +/** + * List available icons + */ +export declare function listIcons(provider?: string, prefix?: string): string[]; + +/** + * Load one icon using Promise + */ +export declare const loadIcon: (icon: IconifyIconName | string) => Promise>; + +/** + * Load icons + */ +export declare const loadIcons: IconifyLoadIcons; + +export declare type PartialIconifyAPIConfig = Partial & Pick; + +/** + * Callback for "abort" pending item. + */ +declare type QueryAbortCallback = () => void; + +/** + * Callback + * + * If error is present, something went wrong and data is undefined. If error is undefined, data is set. + */ +declare type QueryDoneCallback = (data?: QueryModuleResponseData, error?: QueryModuleResponseData) => void; + +declare type QueryModuleResponse = (status: QueryModuleResponseType, data: QueryModuleResponseData) => void; + +/** + * Response from query module + */ +declare type QueryModuleResponseData = unknown; + +/** + * Response from query module + */ +declare type QueryModuleResponseType = 'success' | 'next' | 'abort'; + +/** + * Configuration object + */ +declare interface RedundancyConfig { + resources: RedundancyResource[]; + index: number; + timeout: number; + rotate: number; + random: boolean; + dataAfterTimeout: boolean; +} + +/** + * Resource to rotate (usually hostname or partial URL) + */ +declare type RedundancyResource = string; + +/** + * IDs usage: + * + * id="{id}" + * xlink:href="#{id}" + * url(#{id}) + * + * From SVG animations: + * + * begin="0;{id}.end" + * begin="{id}.end" + * begin="{id}.click" + */ +/** + * Replace IDs in SVG output with unique IDs + */ +export declare function replaceIDs(body: string, prefix?: string | ((id: string) => string)): string; + +/** + * Set custom loader for one icon + */ +export declare function setCustomIconLoader(loader: IconifyCustomIconLoader, prefix: string, provider?: string): void; + +/** + * Set custom loader for multiple icons + */ +export declare function setCustomIconsLoader(loader: IconifyCustomIconsLoader, prefix: string, provider?: string): void; + +/** + * SVG viewBox: x, y, width, height + */ +declare type SVGViewBox = [x: number, y: number, width: number, height: number]; + +export { } diff --git a/node_modules/@iconify/react/dist/iconify.js b/node_modules/@iconify/react/dist/iconify.js new file mode 100644 index 00000000..b27bf7bf --- /dev/null +++ b/node_modules/@iconify/react/dist/iconify.js @@ -0,0 +1,1887 @@ +'use client'; + +import { createElement, forwardRef, useState, useEffect } from 'react'; + +const defaultIconDimensions = Object.freeze( + { + left: 0, + top: 0, + width: 16, + height: 16 + } +); +const defaultIconTransformations = Object.freeze({ + rotate: 0, + vFlip: false, + hFlip: false +}); +const defaultIconProps = Object.freeze({ + ...defaultIconDimensions, + ...defaultIconTransformations +}); +const defaultExtendedIconProps = Object.freeze({ + ...defaultIconProps, + body: "", + hidden: false +}); + +function mergeIconTransformations(obj1, obj2) { + const result = {}; + if (!obj1.hFlip !== !obj2.hFlip) { + result.hFlip = true; + } + if (!obj1.vFlip !== !obj2.vFlip) { + result.vFlip = true; + } + const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4; + if (rotate) { + result.rotate = rotate; + } + return result; +} + +function mergeIconData(parent, child) { + const result = mergeIconTransformations(parent, child); + for (const key in defaultExtendedIconProps) { + if (key in defaultIconTransformations) { + if (key in parent && !(key in result)) { + result[key] = defaultIconTransformations[key]; + } + } else if (key in child) { + result[key] = child[key]; + } else if (key in parent) { + result[key] = parent[key]; + } + } + return result; +} + +function getIconsTree(data, names) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + const resolved = /* @__PURE__ */ Object.create(null); + function resolve(name) { + if (icons[name]) { + return resolved[name] = []; + } + if (!(name in resolved)) { + resolved[name] = null; + const parent = aliases[name] && aliases[name].parent; + const value = parent && resolve(parent); + if (value) { + resolved[name] = [parent].concat(value); + } + } + return resolved[name]; + } + (Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve); + return resolved; +} + +function internalGetIconData(data, name, tree) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + let currentProps = {}; + function parse(name2) { + currentProps = mergeIconData( + icons[name2] || aliases[name2], + currentProps + ); + } + parse(name); + tree.forEach(parse); + return mergeIconData(data, currentProps); +} + +function parseIconSet(data, callback) { + const names = []; + if (typeof data !== "object" || typeof data.icons !== "object") { + return names; + } + if (data.not_found instanceof Array) { + data.not_found.forEach((name) => { + callback(name, null); + names.push(name); + }); + } + const tree = getIconsTree(data); + for (const name in tree) { + const item = tree[name]; + if (item) { + callback(name, internalGetIconData(data, name, item)); + names.push(name); + } + } + return names; +} + +const optionalPropertyDefaults = { + provider: "", + aliases: {}, + not_found: {}, + ...defaultIconDimensions +}; +function checkOptionalProps(item, defaults) { + for (const prop in defaults) { + if (prop in item && typeof item[prop] !== typeof defaults[prop]) { + return false; + } + } + return true; +} +function quicklyValidateIconSet(obj) { + if (typeof obj !== "object" || obj === null) { + return null; + } + const data = obj; + if (typeof data.prefix !== "string" || !obj.icons || typeof obj.icons !== "object") { + return null; + } + if (!checkOptionalProps(obj, optionalPropertyDefaults)) { + return null; + } + const icons = data.icons; + for (const name in icons) { + const icon = icons[name]; + if ( + // Name cannot be empty + !name || // Must have body + typeof icon.body !== "string" || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + for (const name in aliases) { + const icon = aliases[name]; + const parent = icon.parent; + if ( + // Name cannot be empty + !name || // Parent must be set and point to existing icon + typeof parent !== "string" || !icons[parent] && !aliases[parent] || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + return data; +} + +const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const stringToIcon = (value, validate, allowSimpleName, provider = "") => { + const colonSeparated = value.split(":"); + if (value.slice(0, 1) === "@") { + if (colonSeparated.length < 2 || colonSeparated.length > 3) { + return null; + } + provider = colonSeparated.shift().slice(1); + } + if (colonSeparated.length > 3 || !colonSeparated.length) { + return null; + } + if (colonSeparated.length > 1) { + const name2 = colonSeparated.pop(); + const prefix = colonSeparated.pop(); + const result = { + // Allow provider without '@': "provider:prefix:name" + provider: colonSeparated.length > 0 ? colonSeparated[0] : provider, + prefix, + name: name2 + }; + return validate && !validateIconName(result) ? null : result; + } + const name = colonSeparated[0]; + const dashSeparated = name.split("-"); + if (dashSeparated.length > 1) { + const result = { + provider, + prefix: dashSeparated.shift(), + name: dashSeparated.join("-") + }; + return validate && !validateIconName(result) ? null : result; + } + if (allowSimpleName && provider === "") { + const result = { + provider, + prefix: "", + name + }; + return validate && !validateIconName(result, allowSimpleName) ? null : result; + } + return null; +}; +const validateIconName = (icon, allowSimpleName) => { + if (!icon) { + return false; + } + return !!// Check prefix: cannot be empty, unless allowSimpleName is enabled + // Check name: cannot be empty + ((allowSimpleName && icon.prefix === "" || !!icon.prefix) && !!icon.name); +}; + +const dataStorage = /* @__PURE__ */ Object.create(null); +function newStorage(provider, prefix) { + return { + provider, + prefix, + icons: /* @__PURE__ */ Object.create(null), + missing: /* @__PURE__ */ new Set() + }; +} +function getStorage(provider, prefix) { + const providerStorage = dataStorage[provider] || (dataStorage[provider] = /* @__PURE__ */ Object.create(null)); + return providerStorage[prefix] || (providerStorage[prefix] = newStorage(provider, prefix)); +} +function addIconSet(storage, data) { + if (!quicklyValidateIconSet(data)) { + return []; + } + return parseIconSet(data, (name, icon) => { + if (icon) { + storage.icons[name] = icon; + } else { + storage.missing.add(name); + } + }); +} +function addIconToStorage(storage, name, icon) { + try { + if (typeof icon.body === "string") { + storage.icons[name] = { ...icon }; + return true; + } + } catch (err) { + } + return false; +} +function listIcons(provider, prefix) { + let allIcons = []; + const providers = typeof provider === "string" ? [provider] : Object.keys(dataStorage); + providers.forEach((provider2) => { + const prefixes = typeof provider2 === "string" && typeof prefix === "string" ? [prefix] : Object.keys(dataStorage[provider2] || {}); + prefixes.forEach((prefix2) => { + const storage = getStorage(provider2, prefix2); + allIcons = allIcons.concat( + Object.keys(storage.icons).map( + (name) => (provider2 !== "" ? "@" + provider2 + ":" : "") + prefix2 + ":" + name + ) + ); + }); + }); + return allIcons; +} + +let simpleNames = false; +function allowSimpleNames(allow) { + if (typeof allow === "boolean") { + simpleNames = allow; + } + return simpleNames; +} +function getIconData(name) { + const icon = typeof name === "string" ? stringToIcon(name, true, simpleNames) : name; + if (icon) { + const storage = getStorage(icon.provider, icon.prefix); + const iconName = icon.name; + return storage.icons[iconName] || (storage.missing.has(iconName) ? null : void 0); + } +} +function addIcon(name, data) { + const icon = stringToIcon(name, true, simpleNames); + if (!icon) { + return false; + } + const storage = getStorage(icon.provider, icon.prefix); + if (data) { + return addIconToStorage(storage, icon.name, data); + } else { + storage.missing.add(icon.name); + return true; + } +} +function addCollection(data, provider) { + if (typeof data !== "object") { + return false; + } + if (typeof provider !== "string") { + provider = data.provider || ""; + } + if (simpleNames && !provider && !data.prefix) { + let added = false; + if (quicklyValidateIconSet(data)) { + data.prefix = ""; + parseIconSet(data, (name, icon) => { + if (addIcon(name, icon)) { + added = true; + } + }); + } + return added; + } + const prefix = data.prefix; + if (!validateIconName({ + prefix, + name: "a" + })) { + return false; + } + const storage = getStorage(provider, prefix); + return !!addIconSet(storage, data); +} +function iconLoaded(name) { + return !!getIconData(name); +} +function getIcon(name) { + const result = getIconData(name); + return result ? { + ...defaultIconProps, + ...result + } : result; +} + +const defaultIconSizeCustomisations = Object.freeze({ + width: null, + height: null +}); +const defaultIconCustomisations = Object.freeze({ + // Dimensions + ...defaultIconSizeCustomisations, + // Transformations + ...defaultIconTransformations +}); + +const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; +const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; +function calculateSize(size, ratio, precision) { + if (ratio === 1) { + return size; + } + precision = precision || 100; + if (typeof size === "number") { + return Math.ceil(size * ratio * precision) / precision; + } + if (typeof size !== "string") { + return size; + } + const oldParts = size.split(unitsSplit); + if (oldParts === null || !oldParts.length) { + return size; + } + const newParts = []; + let code = oldParts.shift(); + let isNumber = unitsTest.test(code); + while (true) { + if (isNumber) { + const num = parseFloat(code); + if (isNaN(num)) { + newParts.push(code); + } else { + newParts.push(Math.ceil(num * ratio * precision) / precision); + } + } else { + newParts.push(code); + } + code = oldParts.shift(); + if (code === void 0) { + return newParts.join(""); + } + isNumber = !isNumber; + } +} + +function splitSVGDefs(content, tag = "defs") { + let defs = ""; + const index = content.indexOf("<" + tag); + while (index >= 0) { + const start = content.indexOf(">", index); + const end = content.indexOf("", end); + if (endEnd === -1) { + break; + } + defs += content.slice(start + 1, end).trim(); + content = content.slice(0, index).trim() + content.slice(endEnd + 1); + } + return { + defs, + content + }; +} +function mergeDefsAndContent(defs, content) { + return defs ? "" + defs + "" + content : content; +} +function wrapSVGContent(body, start, end) { + const split = splitSVGDefs(body); + return mergeDefsAndContent(split.defs, start + split.content + end); +} + +const isUnsetKeyword = (value) => value === "unset" || value === "undefined" || value === "none"; +function iconToSVG(icon, customisations) { + const fullIcon = { + ...defaultIconProps, + ...icon + }; + const fullCustomisations = { + ...defaultIconCustomisations, + ...customisations + }; + const box = { + left: fullIcon.left, + top: fullIcon.top, + width: fullIcon.width, + height: fullIcon.height + }; + let body = fullIcon.body; + [fullIcon, fullCustomisations].forEach((props) => { + const transformations = []; + const hFlip = props.hFlip; + const vFlip = props.vFlip; + let rotation = props.rotate; + if (hFlip) { + if (vFlip) { + rotation += 2; + } else { + transformations.push( + "translate(" + (box.width + box.left).toString() + " " + (0 - box.top).toString() + ")" + ); + transformations.push("scale(-1 1)"); + box.top = box.left = 0; + } + } else if (vFlip) { + transformations.push( + "translate(" + (0 - box.left).toString() + " " + (box.height + box.top).toString() + ")" + ); + transformations.push("scale(1 -1)"); + box.top = box.left = 0; + } + let tempValue; + if (rotation < 0) { + rotation -= Math.floor(rotation / 4) * 4; + } + rotation = rotation % 4; + switch (rotation) { + case 1: + tempValue = box.height / 2 + box.top; + transformations.unshift( + "rotate(90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + case 2: + transformations.unshift( + "rotate(180 " + (box.width / 2 + box.left).toString() + " " + (box.height / 2 + box.top).toString() + ")" + ); + break; + case 3: + tempValue = box.width / 2 + box.left; + transformations.unshift( + "rotate(-90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + } + if (rotation % 2 === 1) { + if (box.left !== box.top) { + tempValue = box.left; + box.left = box.top; + box.top = tempValue; + } + if (box.width !== box.height) { + tempValue = box.width; + box.width = box.height; + box.height = tempValue; + } + } + if (transformations.length) { + body = wrapSVGContent( + body, + '', + "" + ); + } + }); + const customisationsWidth = fullCustomisations.width; + const customisationsHeight = fullCustomisations.height; + const boxWidth = box.width; + const boxHeight = box.height; + let width; + let height; + if (customisationsWidth === null) { + height = customisationsHeight === null ? "1em" : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + width = calculateSize(height, boxWidth / boxHeight); + } else { + width = customisationsWidth === "auto" ? boxWidth : customisationsWidth; + height = customisationsHeight === null ? calculateSize(width, boxHeight / boxWidth) : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + } + const attributes = {}; + const setAttr = (prop, value) => { + if (!isUnsetKeyword(value)) { + attributes[prop] = value.toString(); + } + }; + setAttr("width", width); + setAttr("height", height); + const viewBox = [box.left, box.top, boxWidth, boxHeight]; + attributes.viewBox = viewBox.join(" "); + return { + attributes, + viewBox, + body + }; +} + +const regex = /\sid="(\S+)"/g; +const randomPrefix = "IconifyId" + Date.now().toString(16) + (Math.random() * 16777216 | 0).toString(16); +let counter = 0; +function replaceIDs(body, prefix = randomPrefix) { + const ids = []; + let match; + while (match = regex.exec(body)) { + ids.push(match[1]); + } + if (!ids.length) { + return body; + } + const suffix = "suffix" + (Math.random() * 16777216 | Date.now()).toString(16); + ids.forEach((id) => { + const newID = typeof prefix === "function" ? prefix(id) : prefix + (counter++).toString(); + const escapedID = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + body = body.replace( + // Allowed characters before id: [#;"] + // Allowed characters after id: [)"], .[a-z] + new RegExp('([#;"])(' + escapedID + ')([")]|\\.[a-z])', "g"), + "$1" + newID + suffix + "$3" + ); + }); + body = body.replace(new RegExp(suffix, "g"), ""); + return body; +} + +const storage = /* @__PURE__ */ Object.create(null); +function setAPIModule(provider, item) { + storage[provider] = item; +} +function getAPIModule(provider) { + return storage[provider] || storage[""]; +} + +function createAPIConfig(source) { + let resources; + if (typeof source.resources === "string") { + resources = [source.resources]; + } else { + resources = source.resources; + if (!(resources instanceof Array) || !resources.length) { + return null; + } + } + const result = { + // API hosts + resources, + // Root path + path: source.path || "/", + // URL length limit + maxURL: source.maxURL || 500, + // Timeout before next host is used. + rotate: source.rotate || 750, + // Timeout before failing query. + timeout: source.timeout || 5e3, + // Randomise default API end point. + random: source.random === true, + // Start index + index: source.index || 0, + // Receive data after time out (used if time out kicks in first, then API module sends data anyway). + dataAfterTimeout: source.dataAfterTimeout !== false + }; + return result; +} +const configStorage = /* @__PURE__ */ Object.create(null); +const fallBackAPISources = [ + "https://api.simplesvg.com", + "https://api.unisvg.com" +]; +const fallBackAPI = []; +while (fallBackAPISources.length > 0) { + if (fallBackAPISources.length === 1) { + fallBackAPI.push(fallBackAPISources.shift()); + } else { + if (Math.random() > 0.5) { + fallBackAPI.push(fallBackAPISources.shift()); + } else { + fallBackAPI.push(fallBackAPISources.pop()); + } + } +} +configStorage[""] = createAPIConfig({ + resources: ["https://api.iconify.design"].concat(fallBackAPI) +}); +function addAPIProvider(provider, customConfig) { + const config = createAPIConfig(customConfig); + if (config === null) { + return false; + } + configStorage[provider] = config; + return true; +} +function getAPIConfig(provider) { + return configStorage[provider]; +} +function listAPIProviders() { + return Object.keys(configStorage); +} + +const detectFetch = () => { + let callback; + try { + callback = fetch; + if (typeof callback === "function") { + return callback; + } + } catch (err) { + } +}; +let fetchModule = detectFetch(); +function setFetch(fetch2) { + fetchModule = fetch2; +} +function getFetch() { + return fetchModule; +} +function calculateMaxLength(provider, prefix) { + const config = getAPIConfig(provider); + if (!config) { + return 0; + } + let result; + if (!config.maxURL) { + result = 0; + } else { + let maxHostLength = 0; + config.resources.forEach((item) => { + const host = item; + maxHostLength = Math.max(maxHostLength, host.length); + }); + const url = prefix + ".json?icons="; + result = config.maxURL - maxHostLength - config.path.length - url.length; + } + return result; +} +function shouldAbort(status) { + return status === 404; +} +const prepare = (provider, prefix, icons) => { + const results = []; + const maxLength = calculateMaxLength(provider, prefix); + const type = "icons"; + let item = { + type, + provider, + prefix, + icons: [] + }; + let length = 0; + icons.forEach((name, index) => { + length += name.length + 1; + if (length >= maxLength && index > 0) { + results.push(item); + item = { + type, + provider, + prefix, + icons: [] + }; + length = name.length; + } + item.icons.push(name); + }); + results.push(item); + return results; +}; +function getPath(provider) { + if (typeof provider === "string") { + const config = getAPIConfig(provider); + if (config) { + return config.path; + } + } + return "/"; +} +const send = (host, params, callback) => { + if (!fetchModule) { + callback("abort", 424); + return; + } + let path = getPath(params.provider); + switch (params.type) { + case "icons": { + const prefix = params.prefix; + const icons = params.icons; + const iconsList = icons.join(","); + const urlParams = new URLSearchParams({ + icons: iconsList + }); + path += prefix + ".json?" + urlParams.toString(); + break; + } + case "custom": { + const uri = params.uri; + path += uri.slice(0, 1) === "/" ? uri.slice(1) : uri; + break; + } + default: + callback("abort", 400); + return; + } + let defaultError = 503; + fetchModule(host + path).then((response) => { + const status = response.status; + if (status !== 200) { + setTimeout(() => { + callback(shouldAbort(status) ? "abort" : "next", status); + }); + return; + } + defaultError = 501; + return response.json(); + }).then((data) => { + if (typeof data !== "object" || data === null) { + setTimeout(() => { + if (data === 404) { + callback("abort", data); + } else { + callback("next", defaultError); + } + }); + return; + } + setTimeout(() => { + callback("success", data); + }); + }).catch(() => { + callback("next", defaultError); + }); +}; +const fetchAPIModule = { + prepare, + send +}; + +function sortIcons(icons) { + const result = { + loaded: [], + missing: [], + pending: [] + }; + const storage = /* @__PURE__ */ Object.create(null); + icons.sort((a, b) => { + if (a.provider !== b.provider) { + return a.provider.localeCompare(b.provider); + } + if (a.prefix !== b.prefix) { + return a.prefix.localeCompare(b.prefix); + } + return a.name.localeCompare(b.name); + }); + let lastIcon = { + provider: "", + prefix: "", + name: "" + }; + icons.forEach((icon) => { + if (lastIcon.name === icon.name && lastIcon.prefix === icon.prefix && lastIcon.provider === icon.provider) { + return; + } + lastIcon = icon; + const provider = icon.provider; + const prefix = icon.prefix; + const name = icon.name; + const providerStorage = storage[provider] || (storage[provider] = /* @__PURE__ */ Object.create(null)); + const localStorage = providerStorage[prefix] || (providerStorage[prefix] = getStorage(provider, prefix)); + let list; + if (name in localStorage.icons) { + list = result.loaded; + } else if (prefix === "" || localStorage.missing.has(name)) { + list = result.missing; + } else { + list = result.pending; + } + const item = { + provider, + prefix, + name + }; + list.push(item); + }); + return result; +} + +function removeCallback(storages, id) { + storages.forEach((storage) => { + const items = storage.loaderCallbacks; + if (items) { + storage.loaderCallbacks = items.filter((row) => row.id !== id); + } + }); +} +function updateCallbacks(storage) { + if (!storage.pendingCallbacksFlag) { + storage.pendingCallbacksFlag = true; + setTimeout(() => { + storage.pendingCallbacksFlag = false; + const items = storage.loaderCallbacks ? storage.loaderCallbacks.slice(0) : []; + if (!items.length) { + return; + } + let hasPending = false; + const provider = storage.provider; + const prefix = storage.prefix; + items.forEach((item) => { + const icons = item.icons; + const oldLength = icons.pending.length; + icons.pending = icons.pending.filter((icon) => { + if (icon.prefix !== prefix) { + return true; + } + const name = icon.name; + if (storage.icons[name]) { + icons.loaded.push({ + provider, + prefix, + name + }); + } else if (storage.missing.has(name)) { + icons.missing.push({ + provider, + prefix, + name + }); + } else { + hasPending = true; + return true; + } + return false; + }); + if (icons.pending.length !== oldLength) { + if (!hasPending) { + removeCallback([storage], item.id); + } + item.callback( + icons.loaded.slice(0), + icons.missing.slice(0), + icons.pending.slice(0), + item.abort + ); + } + }); + }); + } +} +let idCounter = 0; +function storeCallback(callback, icons, pendingSources) { + const id = idCounter++; + const abort = removeCallback.bind(null, pendingSources, id); + if (!icons.pending.length) { + return abort; + } + const item = { + id, + icons, + callback, + abort + }; + pendingSources.forEach((storage) => { + (storage.loaderCallbacks || (storage.loaderCallbacks = [])).push(item); + }); + return abort; +} + +function listToIcons(list, validate = true, simpleNames = false) { + const result = []; + list.forEach((item) => { + const icon = typeof item === "string" ? stringToIcon(item, validate, simpleNames) : item; + if (icon) { + result.push(icon); + } + }); + return result; +} + +// src/config.ts +var defaultConfig = { + resources: [], + index: 0, + timeout: 2e3, + rotate: 750, + random: false, + dataAfterTimeout: false +}; + +// src/query.ts +function sendQuery(config, payload, query, done) { + const resourcesCount = config.resources.length; + const startIndex = config.random ? Math.floor(Math.random() * resourcesCount) : config.index; + let resources; + if (config.random) { + let list = config.resources.slice(0); + resources = []; + while (list.length > 1) { + const nextIndex = Math.floor(Math.random() * list.length); + resources.push(list[nextIndex]); + list = list.slice(0, nextIndex).concat(list.slice(nextIndex + 1)); + } + resources = resources.concat(list); + } else { + resources = config.resources.slice(startIndex).concat(config.resources.slice(0, startIndex)); + } + const startTime = Date.now(); + let status = "pending"; + let queriesSent = 0; + let lastError; + let timer = null; + let queue = []; + let doneCallbacks = []; + if (typeof done === "function") { + doneCallbacks.push(done); + } + function resetTimer() { + if (timer) { + clearTimeout(timer); + timer = null; + } + } + function abort() { + if (status === "pending") { + status = "aborted"; + } + resetTimer(); + queue.forEach((item) => { + if (item.status === "pending") { + item.status = "aborted"; + } + }); + queue = []; + } + function subscribe(callback, overwrite) { + if (overwrite) { + doneCallbacks = []; + } + if (typeof callback === "function") { + doneCallbacks.push(callback); + } + } + function getQueryStatus() { + return { + startTime, + payload, + status, + queriesSent, + queriesPending: queue.length, + subscribe, + abort + }; + } + function failQuery() { + status = "failed"; + doneCallbacks.forEach((callback) => { + callback(void 0, lastError); + }); + } + function clearQueue() { + queue.forEach((item) => { + if (item.status === "pending") { + item.status = "aborted"; + } + }); + queue = []; + } + function moduleResponse(item, response, data) { + const isError = response !== "success"; + queue = queue.filter((queued) => queued !== item); + switch (status) { + case "pending": + break; + case "failed": + if (isError || !config.dataAfterTimeout) { + return; + } + break; + default: + return; + } + if (response === "abort") { + lastError = data; + failQuery(); + return; + } + if (isError) { + lastError = data; + if (!queue.length) { + if (!resources.length) { + failQuery(); + } else { + execNext(); + } + } + return; + } + resetTimer(); + clearQueue(); + if (!config.random) { + const index = config.resources.indexOf(item.resource); + if (index !== -1 && index !== config.index) { + config.index = index; + } + } + status = "completed"; + doneCallbacks.forEach((callback) => { + callback(data); + }); + } + function execNext() { + if (status !== "pending") { + return; + } + resetTimer(); + const resource = resources.shift(); + if (resource === void 0) { + if (queue.length) { + timer = setTimeout(() => { + resetTimer(); + if (status === "pending") { + clearQueue(); + failQuery(); + } + }, config.timeout); + return; + } + failQuery(); + return; + } + const item = { + status: "pending", + resource, + callback: (status2, data) => { + moduleResponse(item, status2, data); + } + }; + queue.push(item); + queriesSent++; + timer = setTimeout(execNext, config.rotate); + query(resource, payload, item.callback); + } + setTimeout(execNext); + return getQueryStatus; +} + +// src/index.ts +function initRedundancy(cfg) { + const config = { + ...defaultConfig, + ...cfg + }; + let queries = []; + function cleanup() { + queries = queries.filter((item) => item().status === "pending"); + } + function query(payload, queryCallback, doneCallback) { + const query2 = sendQuery( + config, + payload, + queryCallback, + (data, error) => { + cleanup(); + if (doneCallback) { + doneCallback(data, error); + } + } + ); + queries.push(query2); + return query2; + } + function find(callback) { + return queries.find((value) => { + return callback(value); + }) || null; + } + const instance = { + query, + find, + setIndex: (index) => { + config.index = index; + }, + getIndex: () => config.index, + cleanup + }; + return instance; +} + +function emptyCallback$1() { +} +const redundancyCache = /* @__PURE__ */ Object.create(null); +function getRedundancyCache(provider) { + if (!redundancyCache[provider]) { + const config = getAPIConfig(provider); + if (!config) { + return; + } + const redundancy = initRedundancy(config); + const cachedReundancy = { + config, + redundancy + }; + redundancyCache[provider] = cachedReundancy; + } + return redundancyCache[provider]; +} +function sendAPIQuery(target, query, callback) { + let redundancy; + let send; + if (typeof target === "string") { + const api = getAPIModule(target); + if (!api) { + callback(void 0, 424); + return emptyCallback$1; + } + send = api.send; + const cached = getRedundancyCache(target); + if (cached) { + redundancy = cached.redundancy; + } + } else { + const config = createAPIConfig(target); + if (config) { + redundancy = initRedundancy(config); + const moduleKey = target.resources ? target.resources[0] : ""; + const api = getAPIModule(moduleKey); + if (api) { + send = api.send; + } + } + } + if (!redundancy || !send) { + callback(void 0, 424); + return emptyCallback$1; + } + return redundancy.query(query, send, callback)().abort; +} + +function emptyCallback() { +} +function loadedNewIcons(storage) { + if (!storage.iconsLoaderFlag) { + storage.iconsLoaderFlag = true; + setTimeout(() => { + storage.iconsLoaderFlag = false; + updateCallbacks(storage); + }); + } +} +function checkIconNamesForAPI(icons) { + const valid = []; + const invalid = []; + icons.forEach((name) => { + (name.match(matchIconName) ? valid : invalid).push(name); + }); + return { + valid, + invalid + }; +} +function parseLoaderResponse(storage, icons, data) { + function checkMissing() { + const pending = storage.pendingIcons; + icons.forEach((name) => { + if (pending) { + pending.delete(name); + } + if (!storage.icons[name]) { + storage.missing.add(name); + } + }); + } + if (data && typeof data === "object") { + try { + const parsed = addIconSet(storage, data); + if (!parsed.length) { + checkMissing(); + return; + } + } catch (err) { + console.error(err); + } + } + checkMissing(); + loadedNewIcons(storage); +} +function parsePossiblyAsyncResponse(response, callback) { + if (response instanceof Promise) { + response.then((data) => { + callback(data); + }).catch(() => { + callback(null); + }); + } else { + callback(response); + } +} +function loadNewIcons(storage, icons) { + if (!storage.iconsToLoad) { + storage.iconsToLoad = icons; + } else { + storage.iconsToLoad = storage.iconsToLoad.concat(icons).sort(); + } + if (!storage.iconsQueueFlag) { + storage.iconsQueueFlag = true; + setTimeout(() => { + storage.iconsQueueFlag = false; + const { provider, prefix } = storage; + const icons2 = storage.iconsToLoad; + delete storage.iconsToLoad; + if (!icons2 || !icons2.length) { + return; + } + const customIconLoader = storage.loadIcon; + if (storage.loadIcons && (icons2.length > 1 || !customIconLoader)) { + parsePossiblyAsyncResponse( + storage.loadIcons(icons2, prefix, provider), + (data) => { + parseLoaderResponse(storage, icons2, data); + } + ); + return; + } + if (customIconLoader) { + icons2.forEach((name) => { + const response = customIconLoader(name, prefix, provider); + parsePossiblyAsyncResponse(response, (data) => { + const iconSet = data ? { + prefix, + icons: { + [name]: data + } + } : null; + parseLoaderResponse(storage, [name], iconSet); + }); + }); + return; + } + const { valid, invalid } = checkIconNamesForAPI(icons2); + if (invalid.length) { + parseLoaderResponse(storage, invalid, null); + } + if (!valid.length) { + return; + } + const api = prefix.match(matchIconName) ? getAPIModule(provider) : null; + if (!api) { + parseLoaderResponse(storage, valid, null); + return; + } + const params = api.prepare(provider, prefix, valid); + params.forEach((item) => { + sendAPIQuery(provider, item, (data) => { + parseLoaderResponse(storage, item.icons, data); + }); + }); + }); + } +} +const loadIcons = (icons, callback) => { + const cleanedIcons = listToIcons(icons, true, allowSimpleNames()); + const sortedIcons = sortIcons(cleanedIcons); + if (!sortedIcons.pending.length) { + let callCallback = true; + if (callback) { + setTimeout(() => { + if (callCallback) { + callback( + sortedIcons.loaded, + sortedIcons.missing, + sortedIcons.pending, + emptyCallback + ); + } + }); + } + return () => { + callCallback = false; + }; + } + const newIcons = /* @__PURE__ */ Object.create(null); + const sources = []; + let lastProvider, lastPrefix; + sortedIcons.pending.forEach((icon) => { + const { provider, prefix } = icon; + if (prefix === lastPrefix && provider === lastProvider) { + return; + } + lastProvider = provider; + lastPrefix = prefix; + sources.push(getStorage(provider, prefix)); + const providerNewIcons = newIcons[provider] || (newIcons[provider] = /* @__PURE__ */ Object.create(null)); + if (!providerNewIcons[prefix]) { + providerNewIcons[prefix] = []; + } + }); + sortedIcons.pending.forEach((icon) => { + const { provider, prefix, name } = icon; + const storage = getStorage(provider, prefix); + const pendingQueue = storage.pendingIcons || (storage.pendingIcons = /* @__PURE__ */ new Set()); + if (!pendingQueue.has(name)) { + pendingQueue.add(name); + newIcons[provider][prefix].push(name); + } + }); + sources.forEach((storage) => { + const list = newIcons[storage.provider][storage.prefix]; + if (list.length) { + loadNewIcons(storage, list); + } + }); + return callback ? storeCallback(callback, sortedIcons, sources) : emptyCallback; +}; +const loadIcon = (icon) => { + return new Promise((fulfill, reject) => { + const iconObj = typeof icon === "string" ? stringToIcon(icon, true) : icon; + if (!iconObj) { + reject(icon); + return; + } + loadIcons([iconObj || icon], (loaded) => { + if (loaded.length && iconObj) { + const data = getIconData(iconObj); + if (data) { + fulfill({ + ...defaultIconProps, + ...data + }); + return; + } + } + reject(icon); + }); + }); +}; + +function setCustomIconsLoader(loader, prefix, provider) { + getStorage(provider || "", prefix).loadIcons = loader; +} +function setCustomIconLoader(loader, prefix, provider) { + getStorage(provider || "", prefix).loadIcon = loader; +} + +function mergeCustomisations(defaults, item) { + const result = { + ...defaults + }; + for (const key in item) { + const value = item[key]; + const valueType = typeof value; + if (key in defaultIconSizeCustomisations) { + if (value === null || value && (valueType === "string" || valueType === "number")) { + result[key] = value; + } + } else if (valueType === typeof result[key]) { + result[key] = key === "rotate" ? value % 4 : value; + } + } + return result; +} + +const separator = /[\s,]+/; +function flipFromString(custom, flip) { + flip.split(separator).forEach((str) => { + const value = str.trim(); + switch (value) { + case "horizontal": + custom.hFlip = true; + break; + case "vertical": + custom.vFlip = true; + break; + } + }); +} + +function rotateFromString(value, defaultValue = 0) { + const units = value.replace(/^-?[0-9.]*/, ""); + function cleanup(value2) { + while (value2 < 0) { + value2 += 4; + } + return value2 % 4; + } + if (units === "") { + const num = parseInt(value); + return isNaN(num) ? 0 : cleanup(num); + } else if (units !== value) { + let split = 0; + switch (units) { + case "%": + split = 25; + break; + case "deg": + split = 90; + } + if (split) { + let num = parseFloat(value.slice(0, value.length - units.length)); + if (isNaN(num)) { + return 0; + } + num = num / split; + return num % 1 === 0 ? cleanup(num) : 0; + } + } + return defaultValue; +} + +function iconToHTML(body, attributes) { + let renderAttribsHTML = body.indexOf("xlink:") === -1 ? "" : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; + for (const attr in attributes) { + renderAttribsHTML += " " + attr + '="' + attributes[attr] + '"'; + } + return '" + body + ""; +} + +function encodeSVGforURL(svg) { + return svg.replace(/"/g, "'").replace(/%/g, "%25").replace(/#/g, "%23").replace(//g, "%3E").replace(/\s+/g, " "); +} +function svgToData(svg) { + return "data:image/svg+xml," + encodeSVGforURL(svg); +} +function svgToURL(svg) { + return 'url("' + svgToData(svg) + '")'; +} + +let policy; +function createPolicy() { + try { + policy = window.trustedTypes.createPolicy("iconify", { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + createHTML: (s) => s + }); + } catch (err) { + policy = null; + } +} +function cleanUpInnerHTML(html) { + if (policy === void 0) { + createPolicy(); + } + return policy ? policy.createHTML(html) : html; +} + +const defaultExtendedIconCustomisations = { + ...defaultIconCustomisations, + inline: false, +}; + +/** + * Default SVG attributes + */ +const svgDefaults = { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', +}; +/** + * Style modes + */ +const commonProps = { + display: 'inline-block', +}; +const monotoneProps = { + backgroundColor: 'currentColor', +}; +const coloredProps = { + backgroundColor: 'transparent', +}; +// Dynamically add common props to variables above +const propsToAdd = { + Image: 'var(--svg)', + Repeat: 'no-repeat', + Size: '100% 100%', +}; +const propsToAddTo = { + WebkitMask: monotoneProps, + mask: monotoneProps, + background: coloredProps, +}; +for (const prefix in propsToAddTo) { + const list = propsToAddTo[prefix]; + for (const prop in propsToAdd) { + list[prefix + prop] = propsToAdd[prop]; + } +} +/** + * Default values for customisations for inline icon + */ +const inlineDefaults = { + ...defaultExtendedIconCustomisations, + inline: true, +}; +/** + * Fix size: add 'px' to numbers + */ +function fixSize(value) { + return value + (value.match(/^[-0-9.]+$/) ? 'px' : ''); +} +/** + * Render icon + */ +const render = ( +// Icon must be validated before calling this function +icon, +// Partial properties +props, +// Icon name +name) => { + // Get default properties + const defaultProps = props.inline + ? inlineDefaults + : defaultExtendedIconCustomisations; + // Get all customisations + const customisations = mergeCustomisations(defaultProps, props); + // Check mode + const mode = props.mode || 'svg'; + // Create style + const style = {}; + const customStyle = props.style || {}; + // Create SVG component properties + const componentProps = { + ...(mode === 'svg' ? svgDefaults : {}), + }; + if (name) { + const iconName = stringToIcon(name, false, true); + if (iconName) { + const classNames = ['iconify']; + const props = [ + 'provider', + 'prefix', + ]; + for (const prop of props) { + if (iconName[prop]) { + classNames.push('iconify--' + iconName[prop]); + } + } + componentProps.className = classNames.join(' '); + } + } + // Get element properties + for (let key in props) { + const value = props[key]; + if (value === void 0) { + continue; + } + switch (key) { + // Properties to ignore + case 'icon': + case 'style': + case 'children': + case 'onLoad': + case 'mode': + case 'ssr': + break; + // Forward ref + case '_ref': + componentProps.ref = value; + break; + // Merge class names + case 'className': + componentProps[key] = + (componentProps[key] ? componentProps[key] + ' ' : '') + + value; + break; + // Boolean attributes + case 'inline': + case 'hFlip': + case 'vFlip': + customisations[key] = + value === true || value === 'true' || value === 1; + break; + // Flip as string: 'horizontal,vertical' + case 'flip': + if (typeof value === 'string') { + flipFromString(customisations, value); + } + break; + // Color: copy to style + case 'color': + style.color = value; + break; + // Rotation as string + case 'rotate': + if (typeof value === 'string') { + customisations[key] = rotateFromString(value); + } + else if (typeof value === 'number') { + customisations[key] = value; + } + break; + // Remove aria-hidden + case 'ariaHidden': + case 'aria-hidden': + if (value !== true && value !== 'true') { + delete componentProps['aria-hidden']; + } + break; + // Copy missing property if it does not exist in customisations + default: + if (defaultProps[key] === void 0) { + componentProps[key] = value; + } + } + } + // Generate icon + const item = iconToSVG(icon, customisations); + const renderAttribs = item.attributes; + // Inline display + if (customisations.inline) { + style.verticalAlign = '-0.125em'; + } + if (mode === 'svg') { + // Add style + componentProps.style = { + ...style, + ...customStyle, + }; + // Add icon stuff + Object.assign(componentProps, renderAttribs); + // Counter for ids based on "id" property to render icons consistently on server and client + let localCounter = 0; + let id = props.id; + if (typeof id === 'string') { + // Convert '-' to '_' to avoid errors in animations + id = id.replace(/-/g, '_'); + } + // Add icon stuff + componentProps.dangerouslySetInnerHTML = { + __html: cleanUpInnerHTML(replaceIDs(item.body, id ? () => id + 'ID' + localCounter++ : 'iconifyReact')), + }; + return createElement('svg', componentProps); + } + // Render with style + const { body, width, height } = icon; + const useMask = mode === 'mask' || + (mode === 'bg' ? false : body.indexOf('currentColor') !== -1); + // Generate SVG + const html = iconToHTML(body, { + ...renderAttribs, + width: width + '', + height: height + '', + }); + // Generate style + componentProps.style = { + ...style, + '--svg': svgToURL(html), + 'width': fixSize(renderAttribs.width), + 'height': fixSize(renderAttribs.height), + ...commonProps, + ...(useMask ? monotoneProps : coloredProps), + ...customStyle, + }; + return createElement('span', componentProps); +}; + +/** + * Initialise stuff + */ +// Enable short names +allowSimpleNames(true); +// Set API module +setAPIModule('', fetchAPIModule); +/** + * Browser stuff + */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + const _window = window; + // Load icons from global "IconifyPreload" + if (_window.IconifyPreload !== void 0) { + const preload = _window.IconifyPreload; + const err = 'Invalid IconifyPreload syntax.'; + if (typeof preload === 'object' && preload !== null) { + (preload instanceof Array ? preload : [preload]).forEach((item) => { + try { + if ( + // Check if item is an object and not null/array + typeof item !== 'object' || + item === null || + item instanceof Array || + // Check for 'icons' and 'prefix' + typeof item.icons !== 'object' || + typeof item.prefix !== 'string' || + // Add icon set + !addCollection(item)) { + console.error(err); + } + } + catch (e) { + console.error(err); + } + }); + } + } + // Set API from global "IconifyProviders" + if (_window.IconifyProviders !== void 0) { + const providers = _window.IconifyProviders; + if (typeof providers === 'object' && providers !== null) { + for (let key in providers) { + const err = 'IconifyProviders[' + key + '] is invalid.'; + try { + const value = providers[key]; + if (typeof value !== 'object' || + !value || + value.resources === void 0) { + continue; + } + if (!addAPIProvider(key, value)) { + console.error(err); + } + } + catch (e) { + console.error(err); + } + } + } + } +} +function IconComponent(props) { + const [mounted, setMounted] = useState(!!props.ssr); + const [abort, setAbort] = useState({}); + // Get initial state + function getInitialState(mounted) { + if (mounted) { + const name = props.icon; + if (typeof name === 'object') { + // Icon as object + return { + name: '', + data: name, + }; + } + const data = getIconData(name); + if (data) { + return { + name, + data, + }; + } + } + return { + name: '', + }; + } + const [state, setState] = useState(getInitialState(!!props.ssr)); + // Cancel loading + function cleanup() { + const callback = abort.callback; + if (callback) { + callback(); + setAbort({}); + } + } + // Change state if it is different + function changeState(newState) { + if (JSON.stringify(state) !== JSON.stringify(newState)) { + cleanup(); + setState(newState); + return true; + } + } + // Update state + function updateState() { + var _a; + const name = props.icon; + if (typeof name === 'object') { + // Icon as object + changeState({ + name: '', + data: name, + }); + return; + } + // New icon or got icon data + const data = getIconData(name); + if (changeState({ + name, + data, + })) { + if (data === undefined) { + // Load icon, update state when done + const callback = loadIcons([name], updateState); + setAbort({ + callback, + }); + } + else if (data) { + // Icon data is available: trigger onLoad callback if present + (_a = props.onLoad) === null || _a === void 0 ? void 0 : _a.call(props, name); + } + } + } + // Mounted state, cleanup for loader + useEffect(() => { + setMounted(true); + return cleanup; + }, []); + // Icon changed or component mounted + useEffect(() => { + if (mounted) { + updateState(); + } + }, [props.icon, mounted]); + // Render icon + const { name, data } = state; + if (!data) { + return props.children + ? props.children + : props.fallback + ? props.fallback + : createElement('span', {}); + } + return render({ + ...defaultIconProps, + ...data, + }, props, name); +} +/** + * Block icon + * + * @param props - Component properties + */ +const Icon = forwardRef((props, ref) => IconComponent({ + ...props, + _ref: ref, +})); +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +const InlineIcon = forwardRef((props, ref) => IconComponent({ + inline: true, + ...props, + _ref: ref, +})); +/** + * Internal API + */ +const _api = { + getAPIConfig, + setAPIModule, + sendAPIQuery, + setFetch, + getFetch, + listAPIProviders, +}; + +export { Icon, InlineIcon, _api, addAPIProvider, addCollection, addIcon, iconToSVG as buildIcon, calculateSize, getIcon, iconLoaded, listIcons, loadIcon, loadIcons, replaceIDs, setCustomIconLoader, setCustomIconsLoader }; diff --git a/node_modules/@iconify/react/dist/offline.cjs b/node_modules/@iconify/react/dist/offline.cjs new file mode 100644 index 00000000..77f02e25 --- /dev/null +++ b/node_modules/@iconify/react/dist/offline.cjs @@ -0,0 +1,823 @@ +'use client'; + +'use strict'; + +var react = require('react'); + +const defaultIconDimensions = Object.freeze( + { + left: 0, + top: 0, + width: 16, + height: 16 + } +); +const defaultIconTransformations = Object.freeze({ + rotate: 0, + vFlip: false, + hFlip: false +}); +const defaultIconProps = Object.freeze({ + ...defaultIconDimensions, + ...defaultIconTransformations +}); +const defaultExtendedIconProps = Object.freeze({ + ...defaultIconProps, + body: "", + hidden: false +}); + +function mergeIconTransformations(obj1, obj2) { + const result = {}; + if (!obj1.hFlip !== !obj2.hFlip) { + result.hFlip = true; + } + if (!obj1.vFlip !== !obj2.vFlip) { + result.vFlip = true; + } + const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4; + if (rotate) { + result.rotate = rotate; + } + return result; +} + +function mergeIconData(parent, child) { + const result = mergeIconTransformations(parent, child); + for (const key in defaultExtendedIconProps) { + if (key in defaultIconTransformations) { + if (key in parent && !(key in result)) { + result[key] = defaultIconTransformations[key]; + } + } else if (key in child) { + result[key] = child[key]; + } else if (key in parent) { + result[key] = parent[key]; + } + } + return result; +} + +function getIconsTree(data, names) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + const resolved = /* @__PURE__ */ Object.create(null); + function resolve(name) { + if (icons[name]) { + return resolved[name] = []; + } + if (!(name in resolved)) { + resolved[name] = null; + const parent = aliases[name] && aliases[name].parent; + const value = parent && resolve(parent); + if (value) { + resolved[name] = [parent].concat(value); + } + } + return resolved[name]; + } + (Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve); + return resolved; +} + +function internalGetIconData(data, name, tree) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + let currentProps = {}; + function parse(name2) { + currentProps = mergeIconData( + icons[name2] || aliases[name2], + currentProps + ); + } + parse(name); + tree.forEach(parse); + return mergeIconData(data, currentProps); +} + +function parseIconSet(data, callback) { + const names = []; + if (typeof data !== "object" || typeof data.icons !== "object") { + return names; + } + if (data.not_found instanceof Array) { + data.not_found.forEach((name) => { + callback(name, null); + names.push(name); + }); + } + const tree = getIconsTree(data); + for (const name in tree) { + const item = tree[name]; + if (item) { + callback(name, internalGetIconData(data, name, item)); + names.push(name); + } + } + return names; +} + +const optionalPropertyDefaults = { + provider: "", + aliases: {}, + not_found: {}, + ...defaultIconDimensions +}; +function checkOptionalProps(item, defaults) { + for (const prop in defaults) { + if (prop in item && typeof item[prop] !== typeof defaults[prop]) { + return false; + } + } + return true; +} +function quicklyValidateIconSet(obj) { + if (typeof obj !== "object" || obj === null) { + return null; + } + const data = obj; + if (typeof data.prefix !== "string" || !obj.icons || typeof obj.icons !== "object") { + return null; + } + if (!checkOptionalProps(obj, optionalPropertyDefaults)) { + return null; + } + const icons = data.icons; + for (const name in icons) { + const icon = icons[name]; + if ( + // Name cannot be empty + !name || // Must have body + typeof icon.body !== "string" || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + for (const name in aliases) { + const icon = aliases[name]; + const parent = icon.parent; + if ( + // Name cannot be empty + !name || // Parent must be set and point to existing icon + typeof parent !== "string" || !icons[parent] && !aliases[parent] || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + return data; +} + +const defaultIconSizeCustomisations = Object.freeze({ + width: null, + height: null +}); +const defaultIconCustomisations = Object.freeze({ + // Dimensions + ...defaultIconSizeCustomisations, + // Transformations + ...defaultIconTransformations +}); + +function mergeCustomisations(defaults, item) { + const result = { + ...defaults + }; + for (const key in item) { + const value = item[key]; + const valueType = typeof value; + if (key in defaultIconSizeCustomisations) { + if (value === null || value && (valueType === "string" || valueType === "number")) { + result[key] = value; + } + } else if (valueType === typeof result[key]) { + result[key] = key === "rotate" ? value % 4 : value; + } + } + return result; +} + +const separator = /[\s,]+/; +function flipFromString(custom, flip) { + flip.split(separator).forEach((str) => { + const value = str.trim(); + switch (value) { + case "horizontal": + custom.hFlip = true; + break; + case "vertical": + custom.vFlip = true; + break; + } + }); +} + +function rotateFromString(value, defaultValue = 0) { + const units = value.replace(/^-?[0-9.]*/, ""); + function cleanup(value2) { + while (value2 < 0) { + value2 += 4; + } + return value2 % 4; + } + if (units === "") { + const num = parseInt(value); + return isNaN(num) ? 0 : cleanup(num); + } else if (units !== value) { + let split = 0; + switch (units) { + case "%": + split = 25; + break; + case "deg": + split = 90; + } + if (split) { + let num = parseFloat(value.slice(0, value.length - units.length)); + if (isNaN(num)) { + return 0; + } + num = num / split; + return num % 1 === 0 ? cleanup(num) : 0; + } + } + return defaultValue; +} + +const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; +const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; +function calculateSize(size, ratio, precision) { + if (ratio === 1) { + return size; + } + precision = precision || 100; + if (typeof size === "number") { + return Math.ceil(size * ratio * precision) / precision; + } + if (typeof size !== "string") { + return size; + } + const oldParts = size.split(unitsSplit); + if (oldParts === null || !oldParts.length) { + return size; + } + const newParts = []; + let code = oldParts.shift(); + let isNumber = unitsTest.test(code); + while (true) { + if (isNumber) { + const num = parseFloat(code); + if (isNaN(num)) { + newParts.push(code); + } else { + newParts.push(Math.ceil(num * ratio * precision) / precision); + } + } else { + newParts.push(code); + } + code = oldParts.shift(); + if (code === void 0) { + return newParts.join(""); + } + isNumber = !isNumber; + } +} + +function splitSVGDefs(content, tag = "defs") { + let defs = ""; + const index = content.indexOf("<" + tag); + while (index >= 0) { + const start = content.indexOf(">", index); + const end = content.indexOf("", end); + if (endEnd === -1) { + break; + } + defs += content.slice(start + 1, end).trim(); + content = content.slice(0, index).trim() + content.slice(endEnd + 1); + } + return { + defs, + content + }; +} +function mergeDefsAndContent(defs, content) { + return defs ? "" + defs + "" + content : content; +} +function wrapSVGContent(body, start, end) { + const split = splitSVGDefs(body); + return mergeDefsAndContent(split.defs, start + split.content + end); +} + +const isUnsetKeyword = (value) => value === "unset" || value === "undefined" || value === "none"; +function iconToSVG(icon, customisations) { + const fullIcon = { + ...defaultIconProps, + ...icon + }; + const fullCustomisations = { + ...defaultIconCustomisations, + ...customisations + }; + const box = { + left: fullIcon.left, + top: fullIcon.top, + width: fullIcon.width, + height: fullIcon.height + }; + let body = fullIcon.body; + [fullIcon, fullCustomisations].forEach((props) => { + const transformations = []; + const hFlip = props.hFlip; + const vFlip = props.vFlip; + let rotation = props.rotate; + if (hFlip) { + if (vFlip) { + rotation += 2; + } else { + transformations.push( + "translate(" + (box.width + box.left).toString() + " " + (0 - box.top).toString() + ")" + ); + transformations.push("scale(-1 1)"); + box.top = box.left = 0; + } + } else if (vFlip) { + transformations.push( + "translate(" + (0 - box.left).toString() + " " + (box.height + box.top).toString() + ")" + ); + transformations.push("scale(1 -1)"); + box.top = box.left = 0; + } + let tempValue; + if (rotation < 0) { + rotation -= Math.floor(rotation / 4) * 4; + } + rotation = rotation % 4; + switch (rotation) { + case 1: + tempValue = box.height / 2 + box.top; + transformations.unshift( + "rotate(90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + case 2: + transformations.unshift( + "rotate(180 " + (box.width / 2 + box.left).toString() + " " + (box.height / 2 + box.top).toString() + ")" + ); + break; + case 3: + tempValue = box.width / 2 + box.left; + transformations.unshift( + "rotate(-90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + } + if (rotation % 2 === 1) { + if (box.left !== box.top) { + tempValue = box.left; + box.left = box.top; + box.top = tempValue; + } + if (box.width !== box.height) { + tempValue = box.width; + box.width = box.height; + box.height = tempValue; + } + } + if (transformations.length) { + body = wrapSVGContent( + body, + '', + "" + ); + } + }); + const customisationsWidth = fullCustomisations.width; + const customisationsHeight = fullCustomisations.height; + const boxWidth = box.width; + const boxHeight = box.height; + let width; + let height; + if (customisationsWidth === null) { + height = customisationsHeight === null ? "1em" : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + width = calculateSize(height, boxWidth / boxHeight); + } else { + width = customisationsWidth === "auto" ? boxWidth : customisationsWidth; + height = customisationsHeight === null ? calculateSize(width, boxHeight / boxWidth) : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + } + const attributes = {}; + const setAttr = (prop, value) => { + if (!isUnsetKeyword(value)) { + attributes[prop] = value.toString(); + } + }; + setAttr("width", width); + setAttr("height", height); + const viewBox = [box.left, box.top, boxWidth, boxHeight]; + attributes.viewBox = viewBox.join(" "); + return { + attributes, + viewBox, + body + }; +} + +const regex = /\sid="(\S+)"/g; +const randomPrefix = "IconifyId" + Date.now().toString(16) + (Math.random() * 16777216 | 0).toString(16); +let counter = 0; +function replaceIDs(body, prefix = randomPrefix) { + const ids = []; + let match; + while (match = regex.exec(body)) { + ids.push(match[1]); + } + if (!ids.length) { + return body; + } + const suffix = "suffix" + (Math.random() * 16777216 | Date.now()).toString(16); + ids.forEach((id) => { + const newID = typeof prefix === "function" ? prefix(id) : prefix + (counter++).toString(); + const escapedID = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + body = body.replace( + // Allowed characters before id: [#;"] + // Allowed characters after id: [)"], .[a-z] + new RegExp('([#;"])(' + escapedID + ')([")]|\\.[a-z])', "g"), + "$1" + newID + suffix + "$3" + ); + }); + body = body.replace(new RegExp(suffix, "g"), ""); + return body; +} + +function iconToHTML(body, attributes) { + let renderAttribsHTML = body.indexOf("xlink:") === -1 ? "" : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; + for (const attr in attributes) { + renderAttribsHTML += " " + attr + '="' + attributes[attr] + '"'; + } + return '" + body + ""; +} + +function encodeSVGforURL(svg) { + return svg.replace(/"/g, "'").replace(/%/g, "%25").replace(/#/g, "%23").replace(//g, "%3E").replace(/\s+/g, " "); +} +function svgToData(svg) { + return "data:image/svg+xml," + encodeSVGforURL(svg); +} +function svgToURL(svg) { + return 'url("' + svgToData(svg) + '")'; +} + +let policy; +function createPolicy() { + try { + policy = window.trustedTypes.createPolicy("iconify", { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + createHTML: (s) => s + }); + } catch (err) { + policy = null; + } +} +function cleanUpInnerHTML(html) { + if (policy === void 0) { + createPolicy(); + } + return policy ? policy.createHTML(html) : html; +} + +const defaultExtendedIconCustomisations = { + ...defaultIconCustomisations, + inline: false, +}; + +const stringToIcon = (value, validate, allowSimpleName, provider = "") => { + const colonSeparated = value.split(":"); + if (value.slice(0, 1) === "@") { + if (colonSeparated.length < 2 || colonSeparated.length > 3) { + return null; + } + provider = colonSeparated.shift().slice(1); + } + if (colonSeparated.length > 3 || !colonSeparated.length) { + return null; + } + if (colonSeparated.length > 1) { + const name2 = colonSeparated.pop(); + const prefix = colonSeparated.pop(); + const result = { + // Allow provider without '@': "provider:prefix:name" + provider: colonSeparated.length > 0 ? colonSeparated[0] : provider, + prefix, + name: name2 + }; + return result; + } + const name = colonSeparated[0]; + const dashSeparated = name.split("-"); + if (dashSeparated.length > 1) { + const result = { + provider, + prefix: dashSeparated.shift(), + name: dashSeparated.join("-") + }; + return result; + } + if (provider === "") { + const result = { + provider, + prefix: "", + name + }; + return result; + } + return null; +}; + +/** + * Default SVG attributes + */ +const svgDefaults = { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', +}; +/** + * Style modes + */ +const commonProps = { + display: 'inline-block', +}; +const monotoneProps = { + backgroundColor: 'currentColor', +}; +const coloredProps = { + backgroundColor: 'transparent', +}; +// Dynamically add common props to variables above +const propsToAdd = { + Image: 'var(--svg)', + Repeat: 'no-repeat', + Size: '100% 100%', +}; +const propsToAddTo = { + WebkitMask: monotoneProps, + mask: monotoneProps, + background: coloredProps, +}; +for (const prefix in propsToAddTo) { + const list = propsToAddTo[prefix]; + for (const prop in propsToAdd) { + list[prefix + prop] = propsToAdd[prop]; + } +} +/** + * Default values for customisations for inline icon + */ +const inlineDefaults = { + ...defaultExtendedIconCustomisations, + inline: true, +}; +/** + * Fix size: add 'px' to numbers + */ +function fixSize(value) { + return value + (value.match(/^[-0-9.]+$/) ? 'px' : ''); +} +/** + * Render icon + */ +const render = ( +// Icon must be validated before calling this function +icon, +// Partial properties +props, +// Icon name +name) => { + // Get default properties + const defaultProps = props.inline + ? inlineDefaults + : defaultExtendedIconCustomisations; + // Get all customisations + const customisations = mergeCustomisations(defaultProps, props); + // Check mode + const mode = props.mode || 'svg'; + // Create style + const style = {}; + const customStyle = props.style || {}; + // Create SVG component properties + const componentProps = { + ...(mode === 'svg' ? svgDefaults : {}), + }; + if (name) { + const iconName = stringToIcon(name); + if (iconName) { + const classNames = ['iconify']; + const props = [ + 'provider', + 'prefix', + ]; + for (const prop of props) { + if (iconName[prop]) { + classNames.push('iconify--' + iconName[prop]); + } + } + componentProps.className = classNames.join(' '); + } + } + // Get element properties + for (let key in props) { + const value = props[key]; + if (value === void 0) { + continue; + } + switch (key) { + // Properties to ignore + case 'icon': + case 'style': + case 'children': + case 'onLoad': + case 'mode': + case 'ssr': + break; + // Forward ref + case '_ref': + componentProps.ref = value; + break; + // Merge class names + case 'className': + componentProps[key] = + (componentProps[key] ? componentProps[key] + ' ' : '') + + value; + break; + // Boolean attributes + case 'inline': + case 'hFlip': + case 'vFlip': + customisations[key] = + value === true || value === 'true' || value === 1; + break; + // Flip as string: 'horizontal,vertical' + case 'flip': + if (typeof value === 'string') { + flipFromString(customisations, value); + } + break; + // Color: copy to style + case 'color': + style.color = value; + break; + // Rotation as string + case 'rotate': + if (typeof value === 'string') { + customisations[key] = rotateFromString(value); + } + else if (typeof value === 'number') { + customisations[key] = value; + } + break; + // Remove aria-hidden + case 'ariaHidden': + case 'aria-hidden': + if (value !== true && value !== 'true') { + delete componentProps['aria-hidden']; + } + break; + // Copy missing property if it does not exist in customisations + default: + if (defaultProps[key] === void 0) { + componentProps[key] = value; + } + } + } + // Generate icon + const item = iconToSVG(icon, customisations); + const renderAttribs = item.attributes; + // Inline display + if (customisations.inline) { + style.verticalAlign = '-0.125em'; + } + if (mode === 'svg') { + // Add style + componentProps.style = { + ...style, + ...customStyle, + }; + // Add icon stuff + Object.assign(componentProps, renderAttribs); + // Counter for ids based on "id" property to render icons consistently on server and client + let localCounter = 0; + let id = props.id; + if (typeof id === 'string') { + // Convert '-' to '_' to avoid errors in animations + id = id.replace(/-/g, '_'); + } + // Add icon stuff + componentProps.dangerouslySetInnerHTML = { + __html: cleanUpInnerHTML(replaceIDs(item.body, id ? () => id + 'ID' + localCounter++ : 'iconifyReact')), + }; + return react.createElement('svg', componentProps); + } + // Render with style + const { body, width, height } = icon; + const useMask = mode === 'mask' || + (mode === 'bg' ? false : body.indexOf('currentColor') !== -1); + // Generate SVG + const html = iconToHTML(body, { + ...renderAttribs, + width: width + '', + height: height + '', + }); + // Generate style + componentProps.style = { + ...style, + '--svg': svgToURL(html), + 'width': fixSize(renderAttribs.width), + 'height': fixSize(renderAttribs.height), + ...commonProps, + ...(useMask ? monotoneProps : coloredProps), + ...customStyle, + }; + return react.createElement('span', componentProps); +}; + +/** + * Storage for icons referred by name + */ +const storage = Object.create(null); +function IconComponent(props) { + const icon = props.icon; + const data = typeof icon === 'string' ? storage[icon] : icon; + if (!data) { + return props.children + ? props.children + : react.createElement('span', {}); + } + return render({ + ...defaultIconProps, + ...data, + }, props, typeof icon === 'string' ? icon : undefined); +} +/** + * Block icon + * + * @param props - Component properties + */ +const Icon = react.memo(react.forwardRef((props, ref) => IconComponent({ + ...props, + _ref: ref, +}))); +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +const InlineIcon = react.memo(react.forwardRef((props, ref) => IconComponent({ + inline: true, + ...props, + _ref: ref, +}))); +/** + * Add icon to storage, allowing to call it by name + * + * @param name + * @param data + */ +function addIcon(name, data) { + storage[name] = data; +} +/** + * Add collection to storage, allowing to call icons by name + * + * @param data Icon set + * @param prefix Optional prefix to add to icon names, true (default) if prefix from icon set should be used. + */ +function addCollection(data, prefix) { + const iconPrefix = typeof prefix === 'string' + ? prefix + : prefix !== false && typeof data.prefix === 'string' + ? data.prefix + ':' + : ''; + quicklyValidateIconSet(data) && + parseIconSet(data, (name, icon) => { + if (icon) { + storage[iconPrefix + name] = icon; + } + }); +} + +exports.Icon = Icon; +exports.InlineIcon = InlineIcon; +exports.addCollection = addCollection; +exports.addIcon = addIcon; diff --git a/node_modules/@iconify/react/dist/offline.d.cts b/node_modules/@iconify/react/dist/offline.d.cts new file mode 100644 index 00000000..c1e580db --- /dev/null +++ b/node_modules/@iconify/react/dist/offline.d.cts @@ -0,0 +1,110 @@ +import type { IconifyIcon } from '@iconify/types'; +import type { IconifyJSON } from '@iconify/types'; +import { IconifyTransformations } from '@iconify/types'; +import type { JSX as JSX_2 } from 'react'; +import type { ReactNode } from 'react'; +import type { SVGProps } from 'react'; + +/** + * Add collection to storage, allowing to call icons by name + * + * @param data Icon set + * @param prefix Optional prefix to add to icon names, true (default) if prefix from icon set should be used. + */ +export declare function addCollection(data: IconifyJSON, prefix?: string | boolean): void; + +/** + * Add icon to storage, allowing to call it by name + * + * @param name + * @param data + */ +export declare function addIcon(name: string, data: IconifyIcon): void; + +/** + * Block icon + * + * @param props - Component properties + */ +export declare const Icon: IconComponentType; + +declare type IconComponentType = (props: IconProps) => JSX_2.Element; + +/** + * React component properties: generic element for Icon component, SVG for generated component + */ +declare type IconifyElementProps = SVGProps; + +export { IconifyIcon } + +/** + * Icon customisations + */ +export declare type IconifyIconCustomisations = IconifyIconCustomisations_2 & { + rotate?: string | number; + inline?: boolean; +}; + +/** + * Icon customisations + */ +declare interface IconifyIconCustomisations_2 extends IconifyTransformations, IconifyIconSizeCustomisations { +} + +/** + * Callback for when icon has been loaded (only triggered for icons loaded from API) + */ +declare type IconifyIconOnLoad = (name: string) => void; + +/** + * Icon properties + */ +export declare interface IconifyIconProps extends IconifyIconCustomisations { + icon: IconifyIcon | string; + mode?: IconifyRenderMode; + color?: string; + flip?: string; + id?: string; + ssr?: boolean; + fallback?: ReactNode; + onLoad?: IconifyIconOnLoad; +} + +/** + * Icon size + */ +export declare type IconifyIconSize = null | string | number; + +/** + * Dimensions + */ +declare interface IconifyIconSizeCustomisations { + width?: IconifyIconSize; + height?: IconifyIconSize; +} + +export { IconifyJSON } + +/** + * Icon render mode + * + * 'style' = 'bg' or 'mask', depending on icon content + * 'bg' = with style using `background` + * 'mask' = with style using `mask` + * 'svg' = + */ +export declare type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'svg'; + +/** + * Mix of icon properties and SVGSVGElement properties + */ +export declare type IconProps = IconifyElementProps & IconifyIconProps; + +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +export declare const InlineIcon: IconComponentType; + +export { } diff --git a/node_modules/@iconify/react/dist/offline.d.ts b/node_modules/@iconify/react/dist/offline.d.ts new file mode 100644 index 00000000..c1e580db --- /dev/null +++ b/node_modules/@iconify/react/dist/offline.d.ts @@ -0,0 +1,110 @@ +import type { IconifyIcon } from '@iconify/types'; +import type { IconifyJSON } from '@iconify/types'; +import { IconifyTransformations } from '@iconify/types'; +import type { JSX as JSX_2 } from 'react'; +import type { ReactNode } from 'react'; +import type { SVGProps } from 'react'; + +/** + * Add collection to storage, allowing to call icons by name + * + * @param data Icon set + * @param prefix Optional prefix to add to icon names, true (default) if prefix from icon set should be used. + */ +export declare function addCollection(data: IconifyJSON, prefix?: string | boolean): void; + +/** + * Add icon to storage, allowing to call it by name + * + * @param name + * @param data + */ +export declare function addIcon(name: string, data: IconifyIcon): void; + +/** + * Block icon + * + * @param props - Component properties + */ +export declare const Icon: IconComponentType; + +declare type IconComponentType = (props: IconProps) => JSX_2.Element; + +/** + * React component properties: generic element for Icon component, SVG for generated component + */ +declare type IconifyElementProps = SVGProps; + +export { IconifyIcon } + +/** + * Icon customisations + */ +export declare type IconifyIconCustomisations = IconifyIconCustomisations_2 & { + rotate?: string | number; + inline?: boolean; +}; + +/** + * Icon customisations + */ +declare interface IconifyIconCustomisations_2 extends IconifyTransformations, IconifyIconSizeCustomisations { +} + +/** + * Callback for when icon has been loaded (only triggered for icons loaded from API) + */ +declare type IconifyIconOnLoad = (name: string) => void; + +/** + * Icon properties + */ +export declare interface IconifyIconProps extends IconifyIconCustomisations { + icon: IconifyIcon | string; + mode?: IconifyRenderMode; + color?: string; + flip?: string; + id?: string; + ssr?: boolean; + fallback?: ReactNode; + onLoad?: IconifyIconOnLoad; +} + +/** + * Icon size + */ +export declare type IconifyIconSize = null | string | number; + +/** + * Dimensions + */ +declare interface IconifyIconSizeCustomisations { + width?: IconifyIconSize; + height?: IconifyIconSize; +} + +export { IconifyJSON } + +/** + * Icon render mode + * + * 'style' = 'bg' or 'mask', depending on icon content + * 'bg' = with style using `background` + * 'mask' = with style using `mask` + * 'svg' = + */ +export declare type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'svg'; + +/** + * Mix of icon properties and SVGSVGElement properties + */ +export declare type IconProps = IconifyElementProps & IconifyIconProps; + +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +export declare const InlineIcon: IconComponentType; + +export { } diff --git a/node_modules/@iconify/react/dist/offline.js b/node_modules/@iconify/react/dist/offline.js new file mode 100644 index 00000000..a3d80a26 --- /dev/null +++ b/node_modules/@iconify/react/dist/offline.js @@ -0,0 +1,818 @@ +'use client'; + +import { createElement, memo, forwardRef } from 'react'; + +const defaultIconDimensions = Object.freeze( + { + left: 0, + top: 0, + width: 16, + height: 16 + } +); +const defaultIconTransformations = Object.freeze({ + rotate: 0, + vFlip: false, + hFlip: false +}); +const defaultIconProps = Object.freeze({ + ...defaultIconDimensions, + ...defaultIconTransformations +}); +const defaultExtendedIconProps = Object.freeze({ + ...defaultIconProps, + body: "", + hidden: false +}); + +function mergeIconTransformations(obj1, obj2) { + const result = {}; + if (!obj1.hFlip !== !obj2.hFlip) { + result.hFlip = true; + } + if (!obj1.vFlip !== !obj2.vFlip) { + result.vFlip = true; + } + const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4; + if (rotate) { + result.rotate = rotate; + } + return result; +} + +function mergeIconData(parent, child) { + const result = mergeIconTransformations(parent, child); + for (const key in defaultExtendedIconProps) { + if (key in defaultIconTransformations) { + if (key in parent && !(key in result)) { + result[key] = defaultIconTransformations[key]; + } + } else if (key in child) { + result[key] = child[key]; + } else if (key in parent) { + result[key] = parent[key]; + } + } + return result; +} + +function getIconsTree(data, names) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + const resolved = /* @__PURE__ */ Object.create(null); + function resolve(name) { + if (icons[name]) { + return resolved[name] = []; + } + if (!(name in resolved)) { + resolved[name] = null; + const parent = aliases[name] && aliases[name].parent; + const value = parent && resolve(parent); + if (value) { + resolved[name] = [parent].concat(value); + } + } + return resolved[name]; + } + (Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve); + return resolved; +} + +function internalGetIconData(data, name, tree) { + const icons = data.icons; + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + let currentProps = {}; + function parse(name2) { + currentProps = mergeIconData( + icons[name2] || aliases[name2], + currentProps + ); + } + parse(name); + tree.forEach(parse); + return mergeIconData(data, currentProps); +} + +function parseIconSet(data, callback) { + const names = []; + if (typeof data !== "object" || typeof data.icons !== "object") { + return names; + } + if (data.not_found instanceof Array) { + data.not_found.forEach((name) => { + callback(name, null); + names.push(name); + }); + } + const tree = getIconsTree(data); + for (const name in tree) { + const item = tree[name]; + if (item) { + callback(name, internalGetIconData(data, name, item)); + names.push(name); + } + } + return names; +} + +const optionalPropertyDefaults = { + provider: "", + aliases: {}, + not_found: {}, + ...defaultIconDimensions +}; +function checkOptionalProps(item, defaults) { + for (const prop in defaults) { + if (prop in item && typeof item[prop] !== typeof defaults[prop]) { + return false; + } + } + return true; +} +function quicklyValidateIconSet(obj) { + if (typeof obj !== "object" || obj === null) { + return null; + } + const data = obj; + if (typeof data.prefix !== "string" || !obj.icons || typeof obj.icons !== "object") { + return null; + } + if (!checkOptionalProps(obj, optionalPropertyDefaults)) { + return null; + } + const icons = data.icons; + for (const name in icons) { + const icon = icons[name]; + if ( + // Name cannot be empty + !name || // Must have body + typeof icon.body !== "string" || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + const aliases = data.aliases || /* @__PURE__ */ Object.create(null); + for (const name in aliases) { + const icon = aliases[name]; + const parent = icon.parent; + if ( + // Name cannot be empty + !name || // Parent must be set and point to existing icon + typeof parent !== "string" || !icons[parent] && !aliases[parent] || // Check other props + !checkOptionalProps( + icon, + defaultExtendedIconProps + ) + ) { + return null; + } + } + return data; +} + +const defaultIconSizeCustomisations = Object.freeze({ + width: null, + height: null +}); +const defaultIconCustomisations = Object.freeze({ + // Dimensions + ...defaultIconSizeCustomisations, + // Transformations + ...defaultIconTransformations +}); + +function mergeCustomisations(defaults, item) { + const result = { + ...defaults + }; + for (const key in item) { + const value = item[key]; + const valueType = typeof value; + if (key in defaultIconSizeCustomisations) { + if (value === null || value && (valueType === "string" || valueType === "number")) { + result[key] = value; + } + } else if (valueType === typeof result[key]) { + result[key] = key === "rotate" ? value % 4 : value; + } + } + return result; +} + +const separator = /[\s,]+/; +function flipFromString(custom, flip) { + flip.split(separator).forEach((str) => { + const value = str.trim(); + switch (value) { + case "horizontal": + custom.hFlip = true; + break; + case "vertical": + custom.vFlip = true; + break; + } + }); +} + +function rotateFromString(value, defaultValue = 0) { + const units = value.replace(/^-?[0-9.]*/, ""); + function cleanup(value2) { + while (value2 < 0) { + value2 += 4; + } + return value2 % 4; + } + if (units === "") { + const num = parseInt(value); + return isNaN(num) ? 0 : cleanup(num); + } else if (units !== value) { + let split = 0; + switch (units) { + case "%": + split = 25; + break; + case "deg": + split = 90; + } + if (split) { + let num = parseFloat(value.slice(0, value.length - units.length)); + if (isNaN(num)) { + return 0; + } + num = num / split; + return num % 1 === 0 ? cleanup(num) : 0; + } + } + return defaultValue; +} + +const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; +const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; +function calculateSize(size, ratio, precision) { + if (ratio === 1) { + return size; + } + precision = precision || 100; + if (typeof size === "number") { + return Math.ceil(size * ratio * precision) / precision; + } + if (typeof size !== "string") { + return size; + } + const oldParts = size.split(unitsSplit); + if (oldParts === null || !oldParts.length) { + return size; + } + const newParts = []; + let code = oldParts.shift(); + let isNumber = unitsTest.test(code); + while (true) { + if (isNumber) { + const num = parseFloat(code); + if (isNaN(num)) { + newParts.push(code); + } else { + newParts.push(Math.ceil(num * ratio * precision) / precision); + } + } else { + newParts.push(code); + } + code = oldParts.shift(); + if (code === void 0) { + return newParts.join(""); + } + isNumber = !isNumber; + } +} + +function splitSVGDefs(content, tag = "defs") { + let defs = ""; + const index = content.indexOf("<" + tag); + while (index >= 0) { + const start = content.indexOf(">", index); + const end = content.indexOf("", end); + if (endEnd === -1) { + break; + } + defs += content.slice(start + 1, end).trim(); + content = content.slice(0, index).trim() + content.slice(endEnd + 1); + } + return { + defs, + content + }; +} +function mergeDefsAndContent(defs, content) { + return defs ? "" + defs + "" + content : content; +} +function wrapSVGContent(body, start, end) { + const split = splitSVGDefs(body); + return mergeDefsAndContent(split.defs, start + split.content + end); +} + +const isUnsetKeyword = (value) => value === "unset" || value === "undefined" || value === "none"; +function iconToSVG(icon, customisations) { + const fullIcon = { + ...defaultIconProps, + ...icon + }; + const fullCustomisations = { + ...defaultIconCustomisations, + ...customisations + }; + const box = { + left: fullIcon.left, + top: fullIcon.top, + width: fullIcon.width, + height: fullIcon.height + }; + let body = fullIcon.body; + [fullIcon, fullCustomisations].forEach((props) => { + const transformations = []; + const hFlip = props.hFlip; + const vFlip = props.vFlip; + let rotation = props.rotate; + if (hFlip) { + if (vFlip) { + rotation += 2; + } else { + transformations.push( + "translate(" + (box.width + box.left).toString() + " " + (0 - box.top).toString() + ")" + ); + transformations.push("scale(-1 1)"); + box.top = box.left = 0; + } + } else if (vFlip) { + transformations.push( + "translate(" + (0 - box.left).toString() + " " + (box.height + box.top).toString() + ")" + ); + transformations.push("scale(1 -1)"); + box.top = box.left = 0; + } + let tempValue; + if (rotation < 0) { + rotation -= Math.floor(rotation / 4) * 4; + } + rotation = rotation % 4; + switch (rotation) { + case 1: + tempValue = box.height / 2 + box.top; + transformations.unshift( + "rotate(90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + case 2: + transformations.unshift( + "rotate(180 " + (box.width / 2 + box.left).toString() + " " + (box.height / 2 + box.top).toString() + ")" + ); + break; + case 3: + tempValue = box.width / 2 + box.left; + transformations.unshift( + "rotate(-90 " + tempValue.toString() + " " + tempValue.toString() + ")" + ); + break; + } + if (rotation % 2 === 1) { + if (box.left !== box.top) { + tempValue = box.left; + box.left = box.top; + box.top = tempValue; + } + if (box.width !== box.height) { + tempValue = box.width; + box.width = box.height; + box.height = tempValue; + } + } + if (transformations.length) { + body = wrapSVGContent( + body, + '', + "" + ); + } + }); + const customisationsWidth = fullCustomisations.width; + const customisationsHeight = fullCustomisations.height; + const boxWidth = box.width; + const boxHeight = box.height; + let width; + let height; + if (customisationsWidth === null) { + height = customisationsHeight === null ? "1em" : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + width = calculateSize(height, boxWidth / boxHeight); + } else { + width = customisationsWidth === "auto" ? boxWidth : customisationsWidth; + height = customisationsHeight === null ? calculateSize(width, boxHeight / boxWidth) : customisationsHeight === "auto" ? boxHeight : customisationsHeight; + } + const attributes = {}; + const setAttr = (prop, value) => { + if (!isUnsetKeyword(value)) { + attributes[prop] = value.toString(); + } + }; + setAttr("width", width); + setAttr("height", height); + const viewBox = [box.left, box.top, boxWidth, boxHeight]; + attributes.viewBox = viewBox.join(" "); + return { + attributes, + viewBox, + body + }; +} + +const regex = /\sid="(\S+)"/g; +const randomPrefix = "IconifyId" + Date.now().toString(16) + (Math.random() * 16777216 | 0).toString(16); +let counter = 0; +function replaceIDs(body, prefix = randomPrefix) { + const ids = []; + let match; + while (match = regex.exec(body)) { + ids.push(match[1]); + } + if (!ids.length) { + return body; + } + const suffix = "suffix" + (Math.random() * 16777216 | Date.now()).toString(16); + ids.forEach((id) => { + const newID = typeof prefix === "function" ? prefix(id) : prefix + (counter++).toString(); + const escapedID = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + body = body.replace( + // Allowed characters before id: [#;"] + // Allowed characters after id: [)"], .[a-z] + new RegExp('([#;"])(' + escapedID + ')([")]|\\.[a-z])', "g"), + "$1" + newID + suffix + "$3" + ); + }); + body = body.replace(new RegExp(suffix, "g"), ""); + return body; +} + +function iconToHTML(body, attributes) { + let renderAttribsHTML = body.indexOf("xlink:") === -1 ? "" : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; + for (const attr in attributes) { + renderAttribsHTML += " " + attr + '="' + attributes[attr] + '"'; + } + return '" + body + ""; +} + +function encodeSVGforURL(svg) { + return svg.replace(/"/g, "'").replace(/%/g, "%25").replace(/#/g, "%23").replace(//g, "%3E").replace(/\s+/g, " "); +} +function svgToData(svg) { + return "data:image/svg+xml," + encodeSVGforURL(svg); +} +function svgToURL(svg) { + return 'url("' + svgToData(svg) + '")'; +} + +let policy; +function createPolicy() { + try { + policy = window.trustedTypes.createPolicy("iconify", { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + createHTML: (s) => s + }); + } catch (err) { + policy = null; + } +} +function cleanUpInnerHTML(html) { + if (policy === void 0) { + createPolicy(); + } + return policy ? policy.createHTML(html) : html; +} + +const defaultExtendedIconCustomisations = { + ...defaultIconCustomisations, + inline: false, +}; + +const stringToIcon = (value, validate, allowSimpleName, provider = "") => { + const colonSeparated = value.split(":"); + if (value.slice(0, 1) === "@") { + if (colonSeparated.length < 2 || colonSeparated.length > 3) { + return null; + } + provider = colonSeparated.shift().slice(1); + } + if (colonSeparated.length > 3 || !colonSeparated.length) { + return null; + } + if (colonSeparated.length > 1) { + const name2 = colonSeparated.pop(); + const prefix = colonSeparated.pop(); + const result = { + // Allow provider without '@': "provider:prefix:name" + provider: colonSeparated.length > 0 ? colonSeparated[0] : provider, + prefix, + name: name2 + }; + return result; + } + const name = colonSeparated[0]; + const dashSeparated = name.split("-"); + if (dashSeparated.length > 1) { + const result = { + provider, + prefix: dashSeparated.shift(), + name: dashSeparated.join("-") + }; + return result; + } + if (provider === "") { + const result = { + provider, + prefix: "", + name + }; + return result; + } + return null; +}; + +/** + * Default SVG attributes + */ +const svgDefaults = { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', +}; +/** + * Style modes + */ +const commonProps = { + display: 'inline-block', +}; +const monotoneProps = { + backgroundColor: 'currentColor', +}; +const coloredProps = { + backgroundColor: 'transparent', +}; +// Dynamically add common props to variables above +const propsToAdd = { + Image: 'var(--svg)', + Repeat: 'no-repeat', + Size: '100% 100%', +}; +const propsToAddTo = { + WebkitMask: monotoneProps, + mask: monotoneProps, + background: coloredProps, +}; +for (const prefix in propsToAddTo) { + const list = propsToAddTo[prefix]; + for (const prop in propsToAdd) { + list[prefix + prop] = propsToAdd[prop]; + } +} +/** + * Default values for customisations for inline icon + */ +const inlineDefaults = { + ...defaultExtendedIconCustomisations, + inline: true, +}; +/** + * Fix size: add 'px' to numbers + */ +function fixSize(value) { + return value + (value.match(/^[-0-9.]+$/) ? 'px' : ''); +} +/** + * Render icon + */ +const render = ( +// Icon must be validated before calling this function +icon, +// Partial properties +props, +// Icon name +name) => { + // Get default properties + const defaultProps = props.inline + ? inlineDefaults + : defaultExtendedIconCustomisations; + // Get all customisations + const customisations = mergeCustomisations(defaultProps, props); + // Check mode + const mode = props.mode || 'svg'; + // Create style + const style = {}; + const customStyle = props.style || {}; + // Create SVG component properties + const componentProps = { + ...(mode === 'svg' ? svgDefaults : {}), + }; + if (name) { + const iconName = stringToIcon(name); + if (iconName) { + const classNames = ['iconify']; + const props = [ + 'provider', + 'prefix', + ]; + for (const prop of props) { + if (iconName[prop]) { + classNames.push('iconify--' + iconName[prop]); + } + } + componentProps.className = classNames.join(' '); + } + } + // Get element properties + for (let key in props) { + const value = props[key]; + if (value === void 0) { + continue; + } + switch (key) { + // Properties to ignore + case 'icon': + case 'style': + case 'children': + case 'onLoad': + case 'mode': + case 'ssr': + break; + // Forward ref + case '_ref': + componentProps.ref = value; + break; + // Merge class names + case 'className': + componentProps[key] = + (componentProps[key] ? componentProps[key] + ' ' : '') + + value; + break; + // Boolean attributes + case 'inline': + case 'hFlip': + case 'vFlip': + customisations[key] = + value === true || value === 'true' || value === 1; + break; + // Flip as string: 'horizontal,vertical' + case 'flip': + if (typeof value === 'string') { + flipFromString(customisations, value); + } + break; + // Color: copy to style + case 'color': + style.color = value; + break; + // Rotation as string + case 'rotate': + if (typeof value === 'string') { + customisations[key] = rotateFromString(value); + } + else if (typeof value === 'number') { + customisations[key] = value; + } + break; + // Remove aria-hidden + case 'ariaHidden': + case 'aria-hidden': + if (value !== true && value !== 'true') { + delete componentProps['aria-hidden']; + } + break; + // Copy missing property if it does not exist in customisations + default: + if (defaultProps[key] === void 0) { + componentProps[key] = value; + } + } + } + // Generate icon + const item = iconToSVG(icon, customisations); + const renderAttribs = item.attributes; + // Inline display + if (customisations.inline) { + style.verticalAlign = '-0.125em'; + } + if (mode === 'svg') { + // Add style + componentProps.style = { + ...style, + ...customStyle, + }; + // Add icon stuff + Object.assign(componentProps, renderAttribs); + // Counter for ids based on "id" property to render icons consistently on server and client + let localCounter = 0; + let id = props.id; + if (typeof id === 'string') { + // Convert '-' to '_' to avoid errors in animations + id = id.replace(/-/g, '_'); + } + // Add icon stuff + componentProps.dangerouslySetInnerHTML = { + __html: cleanUpInnerHTML(replaceIDs(item.body, id ? () => id + 'ID' + localCounter++ : 'iconifyReact')), + }; + return createElement('svg', componentProps); + } + // Render with style + const { body, width, height } = icon; + const useMask = mode === 'mask' || + (mode === 'bg' ? false : body.indexOf('currentColor') !== -1); + // Generate SVG + const html = iconToHTML(body, { + ...renderAttribs, + width: width + '', + height: height + '', + }); + // Generate style + componentProps.style = { + ...style, + '--svg': svgToURL(html), + 'width': fixSize(renderAttribs.width), + 'height': fixSize(renderAttribs.height), + ...commonProps, + ...(useMask ? monotoneProps : coloredProps), + ...customStyle, + }; + return createElement('span', componentProps); +}; + +/** + * Storage for icons referred by name + */ +const storage = Object.create(null); +function IconComponent(props) { + const icon = props.icon; + const data = typeof icon === 'string' ? storage[icon] : icon; + if (!data) { + return props.children + ? props.children + : createElement('span', {}); + } + return render({ + ...defaultIconProps, + ...data, + }, props, typeof icon === 'string' ? icon : undefined); +} +/** + * Block icon + * + * @param props - Component properties + */ +const Icon = memo(forwardRef((props, ref) => IconComponent({ + ...props, + _ref: ref, +}))); +/** + * Inline icon (has negative verticalAlign that makes it behave like icon font) + * + * @param props - Component properties + */ +const InlineIcon = memo(forwardRef((props, ref) => IconComponent({ + inline: true, + ...props, + _ref: ref, +}))); +/** + * Add icon to storage, allowing to call it by name + * + * @param name + * @param data + */ +function addIcon(name, data) { + storage[name] = data; +} +/** + * Add collection to storage, allowing to call icons by name + * + * @param data Icon set + * @param prefix Optional prefix to add to icon names, true (default) if prefix from icon set should be used. + */ +function addCollection(data, prefix) { + const iconPrefix = typeof prefix === 'string' + ? prefix + : prefix !== false && typeof data.prefix === 'string' + ? data.prefix + ':' + : ''; + quicklyValidateIconSet(data) && + parseIconSet(data, (name, icon) => { + if (icon) { + storage[iconPrefix + name] = icon; + } + }); +} + +export { Icon, InlineIcon, addCollection, addIcon }; diff --git a/node_modules/@iconify/react/license.txt b/node_modules/@iconify/react/license.txt new file mode 100644 index 00000000..a583977b --- /dev/null +++ b/node_modules/@iconify/react/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-PRESENT Vjacheslav Trushkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/node_modules/@iconify/react/offline/package.json b/node_modules/@iconify/react/offline/package.json new file mode 100644 index 00000000..2d00762f --- /dev/null +++ b/node_modules/@iconify/react/offline/package.json @@ -0,0 +1,14 @@ +{ + "name": "@iconify/react/offline", + "main": "../dist/offline.js", + "types": "../dist/offline.d.ts", + "exports": { + "./*": "./*", + ".": { + "types": "../dist/offline.d.ts", + "require": "../dist/offline.cjs", + "import": "../dist/offline.js", + "default": "../dist/offline.js" + } + } +} diff --git a/node_modules/@iconify/react/offline/readme.md b/node_modules/@iconify/react/offline/readme.md new file mode 100644 index 00000000..31eceec5 --- /dev/null +++ b/node_modules/@iconify/react/offline/readme.md @@ -0,0 +1,5 @@ +# @iconify/react/offline + +This sub-directory contains `package.json` with entry points for importing `@iconify/react/offline`. + +There is a duplicate entry in `exports` section of `package.json` in the parent directory, but at moment of coding this, TypeScript does not support conditional exports properly, so this directory is used as a duplicate to make everything work with TypeScript. diff --git a/node_modules/@iconify/react/package.json b/node_modules/@iconify/react/package.json new file mode 100644 index 00000000..a4a96b1d --- /dev/null +++ b/node_modules/@iconify/react/package.json @@ -0,0 +1,75 @@ +{ + "name": "@iconify/react", + "description": "Iconify icon component for React.", + "author": "Vjacheslav Trushkin", + "type": "module", + "version": "6.0.0", + "publishConfig": { + "tag": "next" + }, + "license": "MIT", + "bugs": "https://github.com/iconify/iconify/issues", + "homepage": "https://iconify.design/", + "funding": "https://github.com/sponsors/cyberalien", + "repository": { + "type": "git", + "url": "https://github.com/iconify/iconify.git", + "directory": "components/react" + }, + "main": "dist/iconify.js", + "types": "dist/iconify.d.ts", + "exports": { + "./*": "./*", + ".": { + "types": "./dist/iconify.d.ts", + "require": "./dist/iconify.cjs", + "import": "./dist/iconify.js", + "default": "./dist/iconify.js" + }, + "./offline": { + "types": "./dist/offline.d.ts", + "require": "./dist/offline.cjs", + "import": "./dist/offline.js", + "default": "./dist/offline.js" + }, + "./dist/offline": { + "types": "./dist/offline.d.ts", + "require": "./dist/offline.cjs", + "import": "./dist/offline.js", + "default": "./dist/offline.js" + } + }, + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "devDependencies": { + "@microsoft/api-extractor": "^7.52.2", + "@rollup/plugin-node-resolve": "^15.3.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@types/react": "^18.3.20", + "@types/react-dom": "^18.3.5", + "jsdom": "^25.0.1", + "react": "^18.3.1", + "rimraf": "^6.0.1", + "rollup": "^4.38.0", + "typescript": "^5.8.2", + "vitest": "^2.1.9", + "@iconify/core": "^3.1.0", + "@iconify/utils": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16" + }, + "scripts": { + "clean": "rimraf lib dist tsconfig.tsbuildinfo", + "prebuild": "pnpm run clean", + "build": "node build", + "build:lib": "tsc -b tsconfig.src.json", + "build:dist": "rollup -c rollup.config.js", + "prebuild:api": "api-extractor run --local --verbose --config api-extractor.offline.json", + "build:api": "api-extractor run --local --verbose --config api-extractor.iconify.json", + "build:cleanup": "node cleanup", + "test": "vitest" + } +} \ No newline at end of file diff --git a/node_modules/@iconify/react/readme.md b/node_modules/@iconify/react/readme.md new file mode 100644 index 00000000..2af2774b --- /dev/null +++ b/node_modules/@iconify/react/readme.md @@ -0,0 +1,371 @@ +# Iconify for React + +Iconify for React is not yet another icon component! There are many of them already. + +What you get with other components: + +- Limited set of icons. +- Large bundle size because all icons are bundled. + +Iconify icon component is nothing like that. Component does not include any icon data, it is not tied to any specific icon set. Instead, all data is retrieved from public API on demand. + +That means: + +- One syntax for over 200,000 icons from 150+ icon sets. +- Renders SVG. Many components simply render icon fonts, which look ugly. Iconify renders pixel perfect SVG. +- Loads icons on demand. No need to bundle icons, component will automatically load icon data for icons that you use from Iconify API. + +For more information about Iconify project visit [https://iconify.design/](https://iconify.design/). + +For extended documentation visit [Iconify for React documentation](https://iconify.design/docs/icon-components/react/). + +## Installation + +If you are using NPM: + +```bash +npm install --save-dev @iconify/react +``` + +If you are using Yarn: + +```bash +yarn add --dev @iconify/react +``` + +## Usage with API + +Install `@iconify/react` and import `Icon` from it: + +```typescript +import { Icon } from '@iconify/react'; +``` + +Then use `Icon` component with icon name or data as "icon" parameter: + +```jsx + +``` + +Component will automatically retrieve data for "mdi-light:home" from Iconify API and render it. There are over 200,000 icons available on Iconify API from various free and open source icon sets, including all the most popular icon sets. + +## Offline usage + +This icon component is designed to be used with Iconify API, loading icon data on demand instead of bundling it. + +If you want to use icons without Iconify API, [there are many other options available](https://iconify.design/docs/usage/). + +## Icon Names + +Icon name is a string. Few examples: + +- `@api-provider:icon-set-prefix:icon-name` +- `mdi-light:home` (in this example API provider is empty, so it is skipped) + +It has 3 parts, separated by ":": + +- provider points to API source. Starts with "@", can be empty (empty value is used for public Iconify API). +- prefix is name of icon set. +- name is name of icon. + +See [Iconify for React icon names documentation](https://iconify.design/docs/icon-components/react/icon-name.html) for more detailed explanation. + +## Using icon data + +Instead of icon name, you can pass icon data to component: + +```jsx +import { Icon } from '@iconify/react'; +import home from '@iconify-icons/mdi-light/home'; + +function renderHomeIcon() { + return ; +} +``` + +See [icon packages documentation](https://iconify.design/docs/icons/) for more details. + +### Next.js notice + +Example above will currently fail with Next.js. This is because Next.js uses outdated packaging software that does not support ES modules. But do not worry, there is a simple solution: switch to CommonJS icon packages. + +To switch to CommonJS package, replace this line in example above: + +```js +import home from '@iconify-icons/mdi-light/home'; +``` + +with + +```js +import home from '@iconify/icons-mdi-light/home'; +``` + +All icons are available as ES modules for modern bundler and as CommonJS modules for outdated bundlers. ES modules use format `@iconify-icons/{prefix}`, CommonJS modules use `@iconify/icons-{prefix}`. + +For more details, see [icon packages documentation](https://iconify.design/docs/icons/). + +## Vertical alignment + +Icons have 2 modes: inline and block. Difference between modes is `vertical-align` that is added to inline icons. + +Inline icons are aligned slightly below baseline, so they look centred compared to text, like glyph fonts. + +Block icons do not have alignment, like images, which aligns them to baseline by default. + +Alignment option was added to make icons look like continuation of text, behaving like glyph fonts. This should make migration from glyph fonts easier. + +```jsx +import React from 'react'; +import { Icon, InlineIcon } from '@iconify/react'; + +export function inlineDemo() { + return ( +
+

+ Block: + + +

+

+ Inline: + + +

+
+ ); +} +``` + +To toggle between block and inline modes, you can either use `InlineIcon` or use boolean `inline` property: + +```jsx +import React from 'react'; +import { Icon } from '@iconify/react'; + +export function inlineDemo() { + return ( +
+

+ Block: + + +

+

+ Inline: + + +

+
+ ); +} +``` + +Visual example to show the difference between inline and block modes: + +![Inline icon](https://iconify.design/assets/images/inline.png) + +## Icon component properties + +`icon` property is mandatory. It tells component what icon to render. The value can be a string containing the icon name or an object containing the icon data. + +The icon component has the following optional properties: + +- `inline`. Changes icon behaviour to match icon fonts. See "Inline icon" section above. +- `width` and `height`. Icon dimensions. The default values are "1em" for both. See "Dimensions" section below. +- `color`. Icon colour. This is the same as setting colour in style. See "Icon colour" section below. +- `flip`, `hFlip`, `vFlip`. Flip icon horizontally and/or vertically. See "Transformations" section below. +- `rotate`. Rotate icon by 90, 180 or 270 degrees. See "Transformations" section below. +- `align`, `vAlign`, `hAlign`, `slice`. Icon alignment. See "Alignment" section below. +- `onLoad`. Callback function that is called when icon data has been loaded. See "onLoad" section below. + +### Other properties and events + +In addition to the properties mentioned above, the icon component accepts any other properties and events. All other properties and events will be passed to generated `SVG` element, so you can do stuff like assigning `onClick` event, setting the inline style, add title and so on. + +### Dimensions + +By default, icon height is "1em". With is dynamic, calculated using the icon's width to height ratio. This makes it easy to change icon size by changing `font-size` in the stylesheet, just like icon fonts. + +There are several ways to change icon dimensions: + +- Setting `font-size` in style (or `fontSize` if you are using inline style). +- Setting `width` and/or `height` property. + +Values for `width` and `height` can be numbers or strings. + +If you set only one dimension, another dimension will be calculated using the icon's width to height ratio. For example, if the icon size is 16 x 24, you set the height to 48, the width will be set to 32. Calculations work not only with numbers, but also with string values. + +#### Dimensions as numbers + +You can use numbers for `width` and `height`. + +```jsx + +``` + +```jsx + +``` + +Number values are treated as pixels. That means in examples above, values are identical to "24px" and "16px". + +#### Dimensions as strings without units + +If you use strings without units, they are treated the same as numbers in an example above. + +```jsx + +``` + +```jsx + +``` + +#### Dimensions as strings with units + +You can use units in width and height values: + +```jsx + +``` + +Be careful when using `calc`, view port based units or percentages. In SVG element they might not behave the way you expect them to behave and when using such units, you should consider settings both width and height. + +#### Dimensions as 'auto' + +Keyword "auto" sets dimensions to the icon's `viewBox` dimensions. For example, for 24 x 24 icon using `height="auto"` sets height to 24 pixels. + +```jsx + +``` + +### Icon colour + +There are two types of icons: icons that do not have a palette and icons that do have a palette. + +Icons that do have a palette, such as emojis, cannot be customised. Setting colour to such icons will not change anything. + +Icons that do not have a palette can be customised. By default, colour is set to "currentColor", which means the icon's colour matches text colour. To change the colour you can: + +- Set `color` style or use stylesheet to target icon. If you are using the stylesheet, target `svg` element. +- Add `color` property. + +Examples: + +Using `color` property: + +```jsx + + +``` + +Using inline style: + +```jsx + + +``` + +Using stylesheet: + +```jsx + +``` + +```css +.red-icon { + color: red; +} +``` + +### Transformations + +You can rotate and flip the icon. + +This might seem redundant because icon can also be rotated and flipped using CSS transformations. So why do transformation properties exist? Because it is a different type of transformation. + +- CSS transformations transform the entire icon. +- Icon transformations transform the contents of the icon. + +If you have a square icon, this makes no difference. However, if you have an icon that has different width and height values, it makes a huge difference. + +Rotating 16x24 icon by 90 degrees results in: + +- CSS transformation keeps 16x24 bounding box, which might cause the icon to overlap text around it. +- Icon transformation changes bounding box to 24x16, rotating content inside an icon. + +See [icon transformations documentation](https://iconify.design/docs/icon-components/react/transform.html) for more details. + +#### Flipping an icon + +There are several properties available to flip an icon: + +- `hFlip`: boolean property, flips icon horizontally. +- `vFlip`: boolean property, flips icon vertically. +- `flip`: shorthand string property, can flip icon horizontally and/or vertically. + +Examples: + +Flip an icon horizontally: + +```jsx + + +``` + +Flip an icon vertically: + +```jsx + + +``` + +Flip an icon horizontally and vertically (the same as 180 degrees rotation): + +```jsx + + +``` + +#### Rotating an icon + +An icon can be rotated by 90, 180 and 270 degrees. Only contents of the icon are rotated. + +To rotate an icon, use `rotate` property. Value can be a string (degrees or percentages) or a number. + +Number values are 1 for 90 degrees, 2 for 180 degrees, 3 for 270 degrees. + +Examples of 90 degrees rotation: + +```jsx + + + +``` + +### onLoad + +`onLoad` property is an optional callback function. It is called when icon data has been loaded. + +It is not an event, such as `onClick` event for links, it is a simple callback function. + +When `onLoad` is called: + +- If value of icon property is an object, `onLoad` is not called. +- If value of icon property is a string and icon data is available, `onLoad` is called on first render. +- If value of icon property is a string and icon data is not available, `onLoad` is called on first re-render after icon data is retrieved from API. + +What is the purpose of `onLoad`? To let you know when Icon component renders an icon and when it does not render anything. This allows you to do things like adding class name for parent element, such as "container--with-icon" that modify layout if icon is being displayed. + +## Full documentation + +For extended documentation visit [Iconify for React documentation](https://iconify.design/docs/icon-components/react/). + +## License + +React component is released with MIT license. + +© 2019-PRESENT Vjacheslav Trushkin + +See [Iconify icon sets page](https://icon-sets.iconify.design/) for list of collections and their licenses. diff --git a/node_modules/@iconify/react/tsconfig.src.json b/node_modules/@iconify/react/tsconfig.src.json new file mode 100644 index 00000000..cb4449b3 --- /dev/null +++ b/node_modules/@iconify/react/tsconfig.src.json @@ -0,0 +1,17 @@ +{ + "include": ["src/**/*"], + "exclude": ["tests/**/*"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "target": "ES2019", + "module": "ESNext", + "declaration": true, + "sourceMap": false, + "strict": false, + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + } +} diff --git a/node_modules/@iconify/react/tsconfig.tests.json b/node_modules/@iconify/react/tsconfig.tests.json new file mode 100644 index 00000000..01920cd2 --- /dev/null +++ b/node_modules/@iconify/react/tsconfig.tests.json @@ -0,0 +1,18 @@ +{ + "include": ["tests/**/*", "tests/**/*.tsx"], + "exclude": ["src/*"], + "compilerOptions": { + "rootDir": "./tests", + "target": "ES2019", + "module": "ESNext", + "declaration": false, + "sourceMap": false, + "strict": false, + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "jsx": "react", + "noEmit": true + } +} diff --git a/node_modules/@iconify/react/vitest.config.ts b/node_modules/@iconify/react/vitest.config.ts new file mode 100644 index 00000000..b7597da5 --- /dev/null +++ b/node_modules/@iconify/react/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + watch: false, + }, +}); diff --git a/node_modules/@iconify/types/.prettierrc b/node_modules/@iconify/types/.prettierrc new file mode 100644 index 00000000..3f64aa34 --- /dev/null +++ b/node_modules/@iconify/types/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "singleQuote": true, + "useTabs": true, + "semi": true, + "quoteProps": "consistent", + "endOfLine": "lf" +} diff --git a/node_modules/@iconify/types/README.md b/node_modules/@iconify/types/README.md new file mode 100644 index 00000000..3fc174bf --- /dev/null +++ b/node_modules/@iconify/types/README.md @@ -0,0 +1,459 @@ +# Iconify Types + +Type definitions for using Iconify icon sets with TypeScript. + +## Files structure + +Iconify icon sets are available in several formats: + +- Big JSON files that combine many icons in one file +- Node.js packages split into individual icons + +### Icon format + +Each icon is represented by the `IconifyIcon` type. It is a simple object with multiple string, number or boolean attributes. + +The only required attribute is: + +- `body`: string. Value contains inner HTML of an icon as a string, for example ``. + +Optional attributes are represented by type `IconifyOptional`. They are split into several types: dimensions (`IconifyDimenisons` type) and transformations (`IconifyTransformations` type). + +Dimensions attributes: + +- `width`: number. viewBox width, number. If missing, value is set to 16. +- `height`: number. viewBox height, number. If missing, value is set to 16. +- `left`: number. viewBox left, number. If missing, the value is set to 0. +- `top`: number. viewBox top, number. If missing, the value is set to 0. + +Transformations: + +- `rotate`: number. Icon rotation. Iconify icons can be rotated in 90 degrees increments, allowing to reuse the same source icon for multiple icons, such as arrow-up being a copy of arrow-left rotated by 90 degrees. Values are 0 for 0 degrees, 1 for 90 degrees, 2 for 180 degrees, 3 for 270 degrees. The default value is 0. +- `hFlip`: boolean. Horizontal flip. Similar to the rotation transformation, an icon can be flipped horizontally and vertically. It can be used to quickly create aliases, such as arrow-left being an alias of arrow-right, but with hFlip set to true. The default value is false. +- `vFlip`: boolean. Vertical flip. The default value is false. + +Example of icon object: + +```js +const mdiHandIcon = { + body: '', + width: 24, + height: 24, +}; +``` + +### Icon sets format + +Iconify icon sets format is available from multiple sources: + +- NPM package `@iconify/json` that includes all icon sets +- API responses used by SVG framework + +Icon set format structure is available as the `IconifyJSON` type. It is an object with several fields: + +- `prefix`: string. Icon set prefix. +- `icons`: object. Icons data. Value is an object that represents a set of icons, where the key is an icon name and value is `IconifyIcon` object (see "Icon format" above). +- `aliases`: object. Icon aliases, similar to the `icons` object (see "Aliases" section below). + +Example: + +```json +{ + "prefix": "mdi", + "icons": { + "home": { + "body": "", + "width": 24, + "height": 24 + }, + "arrow-left": { + "body": "", + "width": 24, + "height": 24 + } + } +} +``` + +All icon properties except for `body` are optional and are represented by type `IconifyOptional`. Type `IconifyJSON` also extends type `IconifyOptional`, allowing all optional properties to be placed in the root object. + +If an icon is missing a property, look in the root object for the default value. If the root object does not have the default value, use Iconify default value for that property (see list of properties and default values in the "Icon format" section above). + +Default values in the root object make it possible to reduce duplication. + +Example: + +```json +{ + "prefix": "mdi", + "icons": { + "home": { + "body": "" + }, + "arrow-left": { + "body": "" + } + }, + "width": 24, + "height": 24 +} +``` + +In this example, both icons are 24x24, so width and height have been moved to the root object. + +Another example: + +```json +{ + "prefix": "fa-solid", + "icons": { + "arrow-left": { + "body": "", + "width": 448 + }, + "arrow-circle-left": { + "body": "" + }, + "barcode": { + "body": "" + } + }, + "width": 512, + "height": 512 +} +``` + +In this example `arrow-circle-left` and `barcode` have width of 512, `arrow-left` has width of 448. All icons have a height of 512. + +#### Aliases + +In addition to `icons`, another important field in icon set object is `aliases`. + +Aliases object is similar to icons object, except that instead of icon body icons reference another icon. + +Each entry has the same attributes as an icon, except for `body` and has required attribute `parent` that contains the name of the parent icon. The parent icon must be present in the icon set file. + +Example: + +```json +{ + "prefix": "fa", + "icons": { + "automobile": { + "body": "", + "width": 2048, + "height": 1600 + } + }, + "aliases": { + "car": { + "parent": "automobile" + } + } +} +``` + +In this example `car` is an alias of `automobile`, allowing to use the same icon by multiple names. + +Another example: + +```json +{ + "prefix": "fa", + "icons": { + "caret-left": { + "body": "", + "width": 576, + "height": 1280 + } + }, + "aliases": { + "caret-right": { + "parent": "caret-left", + "hFlip": true + } + } +} +``` + +In this example `caret-right` is alias of `caret-left`, but with additional `hFlip` attribute. It is identical to this: + +```json +{ + "prefix": "fa", + "icons": { + "caret-left": { + "body": "" + }, + "caret-right": { + "body": "", + "hFlip": true + } + }, + "width": 576, + "height": 1280 +} +``` + +##### Merging alias attributes + +If both icon and alias have same attribute, following rules apply: + +- `rotate`: attributes are combined. For example, icon has rotate = 1, alias has rotate = 1. Result will have rotate = 2. To prevent overflow, if rotate > 3, rotate = rotate - 4. +- `hFlip` and `vFlip`: attributes are combined. For example, icon has hFlip = true, alias also has hFlip = true (icon.hFlip !== alias.hFlip). Result is false. false + false = false, false + true = true, true + true = false. +- other attributes are overwritten. + +Example: + +```json +{ + "prefix": "fa", + "icons": { + "caret-left": { + "body": "", + "hFlip": true, + "width": 576, + "height": 1280 + } + }, + "aliases": { + "caret-left-compact": { + "parent": "caret-left", + "left": 64, + "width": 448 + }, + "caret-right": { + "parent": "caret-left", + "hFlip": true + } + } +} +``` + +is identical to: + +```json +{ + "prefix": "fa", + "icons": { + "caret-left": { + "body": "", + "hFlip": true + }, + "caret-left-compact": { + "body": "", + "hFlip": true, // from caret-left + "left": 64, // overwritten + "width": 448 // overwritten + }, + "caret-right": { + "body": "" + // hFlip = false, which is default value, so it was removed + } + }, + "width": 576, + "height": 1280 +} +``` + +#### Metadata + +Icon set files might also contain the metadata. That data is used for browsing icons, searching icons, exporting icon sets as fonts. + +Metadata is a combination of several types, represented as type `IconifyMetaData`. + +##### Icon set information + +Icon set information is part of the metadata, it includes information about an author and license. + +Example: + +```json +{ + "prefix": "dashicons", + "info": { + "name": "Dashicons", + "total": 304, + "author": { + "name": "WordPress", + "url": "https://github.com/WordPress/dashicons" + }, + "license": { + "title": "GPL 2.0", + "spdx": "GPL-2.0-only", + "url": "http://www.gnu.org/licenses/gpl-2.0.html" + }, + "version": "0.9.0", + "samples": ["shortcode", "businessperson", "editor-expand"], + "height": 20, + "category": "General", + "palette": false + }, + "icons": { + // Icons here + } +} +``` + +##### Info + +Information block is part of the metadata, it is used for browsing or searching icon sets. It also contains the license for icons in the icon set and information about the author. + +Info block is represented by the type `IconifyInfo`. You can see an example above in "info" property. + +IconifyInfo type has the following properties, most of them are optional: + +- `name`: string. Icon set name. This field is always set. +- `total`: number. The total number of icons, optional. +- `version`: string. The current version, optional. +- `author`: object. Information about the author, always set. Author information has the following properties: + - `name`: string. Author name. This field is always set. + - `url`: string. Link to icon set, optional. Usually links to GitHub repository. +- `license`: object. Information about the license, always set. License information has the following properties: + - `title`: string. License title. This field is always set. + - `spdx`: string. SPDX license identifier, optional. + - `url`: string. Link to the license, optional. +- `samples`: string[]. Value is an array of icon names that should be used as samples when showing the icon set in an icon sets list. +- `height`: number | number[]. Value is a number or array of numbers, values are pixel grids used in the icon set. If any icons in an icon set do not match the grid, this attribute should not be set. +- `displayHeight`: number. The height value that should be used for displaying samples. Value is a number between 16 and 30 (inclusive). + +##### Characters map + +Characters map is part of the metadata, it is used for icon sets that are either imported from icon fonts or intended to be exported to icon font. + +Characters map allows storing characters for export as well as searching icons by character used in an icon font. + +It is a simple object, where the key is character code in hexadecimal form, value is an icon name. + +Important: each icon can have multiple characters! + +Example: + +```json +{ + "prefix": "fa", + "icons": { + // Icons here + }, + "chars": { + "f000": "glass", + "f001": "music", + "f002": "search", + "f003": "envelope-o", + "f004": "heart", + "f005": "star" + // and so on... + } +} +``` + +##### Categories + +Categories are part of the metadata, used to allow filtering icons when showing the entire icons set. + +Categories list is a simple object, where the key is category name, value is the list of icons. + +Important: each icon can belong to multiple categories! + +```json +{ + "prefix": "fa-solid", + "icons": { + // Icons here + }, + "categories": { + "Accessibility": [ + "american-sign-language-interpreting", + "assistive-listening-systems", + "audio-description", + "blind", + "braille", + "closed-captioning", + "deaf", + "low-vision", + "phone-volume", + "question-circle", + "sign-language", + "tty", + "universal-access", + "wheelchair" + ], + "Alert": [ + "bell", + "bell-slash", + "exclamation", + "exclamation-circle", + "exclamation-triangle", + "radiation", + "radiation-alt", + "skull-crossbones" + ] + // and so on... + } +} +``` + +##### Themes + +Themes are part of the metadata, similar to categories, but using prefixes or suffixes to identify icons that belong to a theme. + +This is useful when icon set has variations of icons, such as "baseline-_", "outline-_". + +Example: + +```json +{ + "prefix": "ic", + "icons": { + // Icons here + }, + "themes": { + "baseline": { + "title": "Baseline", + "prefix": "baseline-" + }, + "outline": { + "title": "Outline", + "prefix": "outline-" + }, + "round": { + "title": "Round", + "prefix": "round-" + }, + "sharp": { + "title": "Sharp", + "prefix": "sharp-" + }, + "twotone": { + "title": "Two-Tone", + "prefix": "twotone-" + } + } +} +``` + +Each theme can have one of the attributes: `prefix` or `suffix`. The prefix must end with `-`, suffix must start with `-`. + +In an example above, all icons that start with "baseline-", such as "baseline-home", are considered part of the "Baseline" theme. + +#### All attributes + +For an example of full icon set files that include metadata, look in icon set files in `@iconify/json` package or browse it at GitHub: [https://github.com/iconify/collections-json](https://github.com/iconify/collections-json) + +For an example of individual icons, look in JavaScript files in NPM packages such as `@iconify/icons-mdi`. + +## Usage + +This repository is intended to be used with any Iconify packages. + +At the moment of writing, multiple Iconify packages are written without TypeScript. At the beginning of the year 2020 plan is to rewrite all of them with TypeScript to make sure data is consistent and avoid duplication, this package will be used for sharing types between Iconify packages. + +## License + +This package is licensed under MIT license. + +`SPDX-License-Identifier: MIT` + +Previous versions of this package were dual-licensed under Apache 2.0 and GPL 2.0 licence, which was messy and confusing. This was later changed to MIT for simplicity. + +© 2021 - 2022 Vjacheslav Trushkin / Iconify OÜ diff --git a/node_modules/@iconify/types/license.txt b/node_modules/@iconify/types/license.txt new file mode 100644 index 00000000..0ee4b07d --- /dev/null +++ b/node_modules/@iconify/types/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 - 2022 Vjacheslav Trushkin / Iconify OÜ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/node_modules/@iconify/types/package.json b/node_modules/@iconify/types/package.json new file mode 100644 index 00000000..91aaf244 --- /dev/null +++ b/node_modules/@iconify/types/package.json @@ -0,0 +1,23 @@ +{ + "name": "@iconify/types", + "type": "module", + "description": "Types for Iconify data", + "version": "2.0.0", + "author": "Vjacheslav Trushkin", + "license": "MIT", + "main": "./types.js", + "types": "./types.d.ts", + "bugs": "https://github.com/iconify/iconify/issues", + "homepage": "https://github.com/iconify/iconify", + "repository": { + "type": "git", + "url": "https://github.com/iconify/iconify.git", + "directory": "packages/types" + }, + "devDependencies": { + "typescript": "^4.8.2" + }, + "scripts": { + "test": "tsc --noEmit --strict --typeRoots '[]' types.d.ts" + } +} \ No newline at end of file diff --git a/node_modules/@iconify/types/pnpm-lock.yaml b/node_modules/@iconify/types/pnpm-lock.yaml new file mode 100644 index 00000000..f4dabf8a --- /dev/null +++ b/node_modules/@iconify/types/pnpm-lock.yaml @@ -0,0 +1,15 @@ +lockfileVersion: 5.4 + +specifiers: + typescript: ^4.6.2 + +devDependencies: + typescript: 4.7.4 + +packages: + + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true diff --git a/node_modules/@iconify/types/provider.d.ts b/node_modules/@iconify/types/provider.d.ts new file mode 100644 index 00000000..c313190d --- /dev/null +++ b/node_modules/@iconify/types/provider.d.ts @@ -0,0 +1,44 @@ +/** + * Raw data sent by API + */ + +// Links +export interface APIProviderRawDataLinks { + // Collections list + home?: string; + // Collection. Available variables: {prefix} + collection?: string; + // Icon. Available variables: {prefix}, {name} + icon?: string; +} + +// NPM +export interface APIProviderRawDataNPM { + // Package name for installation. Available variables: {prefix} + package?: string; + + // Icon import source. Available variables: {prefix}, {name} + icon?: string; +} + +// Main type +export interface APIProviderRawData { + // Provider name (as used in icon names) + provider: string; + + // Provider name (human readable version) + title?: string; + + // API link(s), though they are usually redundant because API end point is used to retrieve data + api?: string | string[]; + + // Links to website + links?: APIProviderRawDataLinks; + + // NPM packages for icons, used when showing code samples + npm?: APIProviderRawDataNPM; + + // SVG generator URL, including full host name, {prefix} and {name} variables + // Example: 'https://api.iconify.design/{prefix}/{name}.svg' + svg?: string; +} diff --git a/node_modules/@iconify/types/provider.js b/node_modules/@iconify/types/provider.js new file mode 100644 index 00000000..059949c0 --- /dev/null +++ b/node_modules/@iconify/types/provider.js @@ -0,0 +1,3 @@ +/** + * Empty file. This repository contains only TypeScript types + */ diff --git a/node_modules/@iconify/types/types.d.ts b/node_modules/@iconify/types/types.d.ts new file mode 100644 index 00000000..0d844fa4 --- /dev/null +++ b/node_modules/@iconify/types/types.d.ts @@ -0,0 +1,272 @@ +/** + * Icon dimensions. + * + * Used in: + * icon (as is) + * alias (overwrite icon's properties) + * root of JSON file (default values) + */ +export interface IconifyDimenisons { + // Left position of viewBox. + // Defaults to 0. + left?: number; + + // Top position of viewBox. + // Defaults to 0. + top?: number; + + // Width of viewBox. + // Defaults to 16. + width?: number; + + // Height of viewBox. + // Defaults to 16. + height?: number; +} + +/** + * Icon transformations. + * + * Used in: + * icon (as is) + * alias (merged with icon's properties) + */ +export interface IconifyTransformations { + // Number of 90 degrees rotations. + // 0 = 0, 1 = 90deg and so on. + // Defaults to 0. + // When merged (such as alias + icon), result is icon.rotation + alias.rotation. + rotate?: number; + + // Horizontal flip. + // Defaults to false. + // When merged, result is icon.hFlip !== alias.hFlip + hFlip?: boolean; + + // Vertical flip. (see hFlip comments) + vFlip?: boolean; +} + +/** + * Combination of dimensions and transformations. + */ +export interface IconifyOptional + extends IconifyDimenisons, + IconifyTransformations { + // +} + +/** + * Alias. + */ +export interface IconifyAlias extends IconifyOptional { + // Parent icon index without prefix, required. + parent: string; + + // IconifyOptional properties. + // Alias should have only properties that it overrides. + // Transformations are merged, not overridden. See IconifyTransformations comments. +} + +/** + * Icon. + */ +export interface IconifyIcon extends IconifyOptional { + // Icon body: , required. + body: string; + + // IconifyOptional properties. + // If property is missing in JSON file, look in root object for default value. +} + +/** + * Icon with optional parameters that are provided by API and affect only search + */ +interface APIIconAttributes { + // True if icon is hidden. + // Used in icon sets to keep icons that no longer exist, but should still be accessible + // from API, preventing websites from breaking when icon is removed by developer. + hidden?: boolean; +} + +export interface ExtendedIconifyIcon extends IconifyIcon, APIIconAttributes {} +export interface ExtendedIconifyAlias extends IconifyAlias, APIIconAttributes {} + +/** + * "icons" field of JSON file. + */ +export interface IconifyIcons { + // Index is name of icon, without prefix. Value is ExtendedIconifyIcon object. + [index: string]: ExtendedIconifyIcon; +} + +/** + * "aliases" field of JSON file. + */ +export interface IconifyAliases { + // Index is name of icon, without prefix. Value is ExtendedIconifyAlias object. + [index: string]: ExtendedIconifyAlias; +} + +/** + * Icon set information block. + */ +export interface IconifyInfo { + // Icon set name. + name: string; + + // Total number of icons. + total?: number; + + // Version string. + version?: string; + + // Author information. + author: { + // Author name. + name: string; + + // Link to author's website or icon set website. + url?: string; + }; + + // License + license: { + // Human readable license. + title: string; + + // SPDX license identifier. + spdx?: string; + + // License URL. + url?: string; + }; + + // Array of icons that should be used for samples in icon sets list. + samples?: string[]; + + // Icon grid: number or array of numbers. + height?: number | number[]; + + // Display height for samples: 16 - 24 + displayHeight?: number; + + // Category on Iconify collections list. + category?: string; + + // List of tags to group similar icon sets. + tags?: string[]; + + // Palette status. True if icons have predefined color scheme, false if icons use currentColor. + // Ideally, icon set should not mix icons with and without palette to simplify search. + palette?: boolean; + + // If true, icon set should not appear in icon sets list. + hidden?: boolean; +} + +/** + * Optional themes, old format. + * + * Deprecated because format is unnecessary complicated. Key is meaningless, suffixes and prefixes are mixed together. + */ +export interface LegacyIconifyThemes { + // Key is unique string. + [index: string]: { + // Theme title. + title: string; + + // Icon prefix or suffix, including dash. All icons that start with prefix and end with suffix belong to theme. + prefix?: string; // Example: 'baseline-' + suffix?: string; // Example: '-filled' + }; +} + +/** + * Characters used in font. + */ +export interface IconifyChars { + // Index is character, such as "f000". + // Value is icon name. + [index: string]: string; +} + +/** + * Icon categories + */ +export interface IconifyCategories { + // Index is category title, such as "Weather". + // Value is array of icons that belong to that category. + // Each icon can belong to multiple categories or no categories. + [index: string]: string[]; +} + +/** + * Meta data stored in JSON file, used for browsing icon set. + */ +export interface IconifyMetaData { + // Icon set information block. Used for public icon sets, can be skipped for private icon sets. + info?: IconifyInfo; + + // Characters used in font. Used for searching by character for icon sets imported from font, exporting icon set to font. + chars?: IconifyChars; + + // Categories. Used for filtering icons. + categories?: IconifyCategories; + + // Optional themes (old format). + themes?: LegacyIconifyThemes; + + // Optional themes (new format). Key is prefix or suffix, value is title. + prefixes?: Record; + suffixes?: Record; +} + +/** + * JSON structure, contains only icon data + */ +export interface IconifyJSONIconsData extends IconifyDimenisons { + // Prefix for icons in JSON file, required. + prefix: string; + + // API provider, optional. + provider?: string; + + // List of icons, required. + icons: IconifyIcons; + + // Optional aliases. + aliases?: IconifyAliases; + + // IconifyDimenisons properties that are used as default viewbox for icons when icon is missing value. + // If viewbox exists in both icon and root, use value from icon. + // This is used to reduce duplication. +} + +/** + * JSON structure. + * + * All optional values can exist in root of JSON file, used as defaults. + */ +export interface IconifyJSON extends IconifyJSONIconsData, IconifyMetaData { + // Last modification time of icons. Unix time stamp in seconds. + // Time is calculated only for icon data, ignoring metadata. + // Used to invalidate icons cache in components. + lastModified?: number; + + // Optional list of missing icons. Returned by Iconify API when querying for icons that do not exist. + not_found?: string[]; +} + +/** + * Structure of exports '@iconify-json/*' packages. + * + * These are small packages, one per icon set, that split JSON structure into multiple files to reduce + * amount of data imported from package. + */ +export interface IconifyJSONPackageExports { + info: IconifyInfo; + icons: IconifyJSON; + metadata: IconifyMetaData; + chars: IconifyChars; +} diff --git a/node_modules/@iconify/types/types.js b/node_modules/@iconify/types/types.js new file mode 100644 index 00000000..059949c0 --- /dev/null +++ b/node_modules/@iconify/types/types.js @@ -0,0 +1,3 @@ +/** + * Empty file. This repository contains only TypeScript types + */ diff --git a/package.json b/package.json index e34f042e..bf358775 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "NODE_ENV=production bun src/index.tsx" }, "dependencies": { + "@iconify/react": "^6.0.0", "@mui/icons-material": "^7.3.1", "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0", diff --git a/src/GarageApp/App.tsx b/src/GarageApp/App.tsx index 633bc233..f6464ef9 100644 --- a/src/GarageApp/App.tsx +++ b/src/GarageApp/App.tsx @@ -2,6 +2,8 @@ import { APITester } from "../APITester"; import "./index.css"; import { useTranslation, withTranslation, Trans } from 'react-i18next'; +import AppBar from './modules/MenuBar'; + import logo from "./logo.svg"; import reactLogo from "./react.svg"; @@ -9,28 +11,18 @@ import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid'; -import VehicleCard from "./modules/VehicleCards"; import YourVehicleList from "./modules/YourVehicles"; +import MenuLanguages from "./modules/MenuLanguages"; +import StatisticsView from "./modules/StatisticsView" export function App() { const { t, i18n } = useTranslation(); return (
-
- Bun Logo - React Logo -
- -

Bun + React | GarageApp

-

- Edit src/App.tsx and save to test HMR -

- -

{t ('statistics')}

- + + + - -

test

); } diff --git a/src/GarageApp/i18n.js b/src/GarageApp/i18n.js index dabb551c..d0ca8cbd 100644 --- a/src/GarageApp/i18n.js +++ b/src/GarageApp/i18n.js @@ -15,7 +15,7 @@ i18n backend: { loadPath: '/GarageApp/locales/{{lng}}/{{ns}}.json' }, - lng: 'da', + lng: 'en', ns: ['translation'] }); diff --git a/src/GarageApp/index.css b/src/GarageApp/index.css index d2ae2a0e..0474a2e1 100644 --- a/src/GarageApp/index.css +++ b/src/GarageApp/index.css @@ -8,7 +8,7 @@ body { margin: 0; display: grid; - place-items: center; + place-items: stretch; min-width: 320px; min-height: 100vh; position: relative; @@ -34,9 +34,7 @@ body::before { } } .app { - max-width: 1280px; margin: 0 auto; - padding: 2rem; text-align: center; position: relative; z-index: 1; @@ -48,8 +46,20 @@ body::before { gap: 2rem; margin-bottom: 2rem; } +.menu-logo-container { + display: flex; + justify-content: left; + align-items: center; + gap: 0.4em; +} +.menu-logo { + height: 2em; + padding: 1em + will-change: filter; + transition: filter 0.3s; +} .logo { - height: 6em; + height: 2em; padding: 1.5em; will-change: filter; transition: filter 0.3s; diff --git a/src/GarageApp/locales/de/translation.json b/src/GarageApp/locales/de/translation.json new file mode 100644 index 00000000..9d9dd5b4 --- /dev/null +++ b/src/GarageApp/locales/de/translation.json @@ -0,0 +1,217 @@ +{ + "quickentry": "Keine Schnelleinträge | Schnelleintrag | Schnelleinträge", + "statistics": "Statistiken", + "thisweek": "Diese Woche", + "thismonth": "Dieser Monat", + "pastxdays": "Letzter Tag | Letzte {count} Tage", + "pastxmonths": "Letzter Monat | Letzte {count} Monate", + "thisyear": "Dieses Jahr", + "alltime": "Gesamt", + "noattachments": "Keine Anhänge", + "attachments": "Anhänge", + "choosefile": "Datei auswählen", + "addattachment": "Anhang hinzufügen", + "sharedwith": "Geteilt mit", + "share": "Teile", + "you": "du", + "addfillup": "Tankfüllung erfassen", + "createfillup": "Erfasse Tankfüllung", + "deletefillup": "Lösche diese Tankfüllung", + "addexpense": "Ausgabe erfassen", + "createexpense": "Erfasse Ausgabe", + "deleteexpense": "Lösche diese Ausgabe", + "nofillups": "Keine Tankfüllungen", + "transfervehicle": "Fahrzeug übertragen", + "settingssaved": "Einstellungen erfolgreich gespeichert", + "yoursettings": "Deine Einstellungen", + "settings": "Einstellungen", + "changepassword": "Passwort ändern", + "oldpassword": "Bisheriges Passwort", + "newpassword": "Neues Passwort", + "repeatnewpassword": "Neues Passwort wiederholen", + "passworddontmatch": "Passwörter stimmen nicht überein", + "save": "Speichern", + "supportthedeveloper": "Unterstütze den Entwickler", + "buyhimabeer": "Kauf ihm ein Bier!", + "moreinfo": "Mehr Info", + "currency": "Währung", + "distanceunit": "Entfernungseinheit", + "dateformat": "Datumsformat", + "createnow": "Jetzt erstellen", + "yourvehicles": "Deine Fahrzeuge", + "menu": { + "quickentries": "Schnellinträge", + "logout": "Abmelden", + "import": "Import", + "home": "Start", + "settings": "Einstellungen", + "admin": "Verwalten", + "sitesettings": "Globale Einstellungen", + "users": "Benutzer", + "login": "Anmelden" + }, + "enterusername": "E-Mail eingeben", + "enterpassword": "Passwort eingeben", + "email": "E-Mail", + "password": "Passwort", + "login": "Anmelden", + "totalexpenses": "Gesamtausgaben", + "fillupcost": "Tank-Ausgaben", + "otherexpenses": "Andere Ausgaben", + "addvehicle": "Fahrzeug hinzufügen", + "editvehicle": "Fahrzeug bearbeiten", + "deletevehicle": "Fahrzeug löschen", + "sharevehicle": "Fahrzeug teilen", + "makeowner": "zum Besitzer machen", + "lastfillup": "Letztes Tanken", + "quickentrydesc": "Mach ein Foto deiner Rechnung oder der Zapfsäule um den Eintrag später zu ergänzen.", + "quickentrycreatedsuccessfully": "Schnelleintrag erfolgreich erstellt", + "uploadfile": "Datei hochladen", + "uploadphoto": "Foto hochladen", + "details": "Details", + "odometer": "Kilometerzähler", + "language": "Sprache", + "date": "Datum", + "pastfillups": "Tankfüllungen", + "fuelsubtype": "Kraftstofftyp", + "fueltype": "Kraftstoff", + "quantity": "Menge", + "gasstation": "Tankstelle", + "fuel": { + "petrol": "Benzin", + "diesel": "Diesel", + "cng": "CNG", + "lpg": "LPG", + "electric": "Strom", + "ethanol": "Ethanol" + }, + "unit": { + "long": { + "litre": "Liter", + "gallon": "Gallone", + "kilowatthour": "Kilowattstunde", + "kilogram": "Kilogramm", + "usgallon": "US-Gallone", + "minutes": "Minuten", + "kilometers": "Kilometer", + "miles": "Meilen" + }, + "short": { + "litre": "L", + "gallon": "Gal", + "kilowatthour": "KwH", + "kilogram": "Kg", + "usgallon": "US-Gal", + "minutes": "Min", + "kilometers": "Km", + "miles": "Mi" + } + }, + "avgfillupqty": "Ø Tankmenge", + "avgfillupexpense": "Ø Tankwert", + "avgfuelcost": "Ø Spritpreis", + "per": "{0} pro {1}", + "price": "Preis", + "total": "Gesamt", + "fulltank": "Voller Tank", + "getafulltank": "Hast du vollgetankt?", + "by": "Von", + "expenses": "Ausgaben", + "expensetype": "Ausgaben Typ", + "noexpenses": "Keine Ausgaben", + "download": "Herunterladen", + "title": "Titel", + "name": "Name", + "delete": "Löschen", + "importdata": "Importiere Daten in Hammond", + "importdatadesc": "Wähle eine der folgenden Optionen, um Daten in Hammond zu importieren", + "import": "Importieren", + "importcsv": "Wenn du {name} nutzt, um deine Fahrzeugdaten zu verwalten, exportiere die CSV Datei aus {name} und klicke hier, um zu importieren.", + "choosecsv": "CSV auswählen", + "choosephoto": "Foto auswählen", + "importsuccessfull": "Daten erfolgreich importiert", + "importerror": "Beim Importieren der Datei ist ein Fehler aufgetreten. Details findest du in der Fehlermeldung", + "importfrom": "Importiere von {name}", + "stepstoimport": "Schritte, um Daten aus {name} zu importieren", + "choosecsvimport": "Wähle die {name} CSV aus und klicke den Button, um zu importieren.", + "dontimportagain": "Achte darauf, dass du die Datei nicht erneut importierst, da dies zu mehrfachen Einträgen führen würde.", + "checkpointsimportcsv": "Wenn du alle diese Punkte überprüft hast kannst du unten die CSV importieren.", + "importhintunits": "Vergewissere dich ebenfalls, dass die Kraftstoffeinheit und der Kraftstofftyp im Fahrzeug richtig eingestellt sind.", + "importhintcurrdist": "Stelle sicher, dass die Währung und die Entfernungseinheit in Hammond korrekt eingestellt sind. Der Import erkennt die Währung nicht automatisch aus der datei, sondern verwendet die für den Benutzer eingestellte Währung.", + "importhintnickname": "Vergewissere dich, dass der Fahrzeugname in Hammond genau mit dem Namen in der Fuelly-CSV-Datei übereinstimmt, sonst funktioniert der Import nicht.", + "importhintvehiclecreated": "Vergewissere dich, dass du die Fahrzeuge bereits in Hammond erstellt hast.", + "importhintcreatecsv": "Exportiere deine Daten aus {name} im CSV-Format. Die Schritte dazu findest du", + "here": "hier", + "unprocessedquickentries": "Du hast einen Schnelleintrag zum bearbeiten. | Du hast {0} Schnelleinträge zum bearbeiten.", + "show": "Anzeigen", + "loginerror": "Bei der Anmeldung ist ein Fehler aufgetreten. {msg}", + "showunprocessed": "Zeige unbearbeitete", + "unprocessed": "unbearbeitet", + "sitesettingdesc": "Ändere die globalen Einstellungen. Diese werden als Standard für neue Benutzer verwendet.", + "settingdesc": "Diese Einstellungen werden als Standard verwendet wenn du eine neue Ausgabe oder eine Tankfüllung erfasst.", + "areyousure": "Bist du dir sicher?", + "adduser": "Benutzer hinzufügen", + "usercreatedsuccessfully": "Benutzer erfolgreich gespeichert", + "role": "Rolle", + "created": "Erstellt", + "createnewuser": "Erstelle neuen Benutzer", + "cancel": "Abbrechen", + "novehicles": "Du hast noch kein Fahrzeug erstellt. Lege jetzt einen Eintrag für das zu verwaltende Fahrzeug an.", + "processed": "Bearbeitet", + "notfound": "Nicht gefunden", + "timeout": "Das Laden der Seite hat eine Zeitüberschreitung verursacht. Bist du sicher, dass du noch mit dem Internet verbunden bist?", + "clicktoselect": "Klicke, um auszuwählen...", + "expenseby": "Ausgabe von", + "selectvehicle": "Wähle ein Fahrzeug aus", + "expensedate": "Datum der Ausgabe", + "totalamountpaid": "Gezahlter Gesamtbetrag", + "fillmoredetails": "Weitere Details ausfüllen", + "markquickentryprocessed": "Markiere gewählten Schnelleintrag als bearbeitet", + "referquickentry": "Wähle Schnelleintrag", + "deletequickentry": "Willst du diesen Schnelleintrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!", + "fuelunit": "Kraftstoffeinheit", + "fillingstation": "Tankstelle", + "comments": "Kommentare", + "missfillupbefore": "Hast du vergessen, die vorherige Tankfüllung zu erfassen?", + "fillupdate": "Tankdatum", + "fillupsavedsuccessfully": "Tankfüllung erfolgreich gespeichert", + "expensesavedsuccessfully": "Ausgabe erfolgreich gespeichert", + "vehiclesavedsuccessfully": "Fahrzeug erfolgreich gespeichert", + "back": "Zurück", + "nickname": "Bezeichnung", + "registration": "Nummernschild", + "createvehicle": "Fahrzeug erstellen", + "make": "Marke", + "model": "Modell", + "yearmanufacture": "Jahr der Erstzulassung", + "enginesize": "Hubraum (in ccm)", + "testconn": "Teste Verbindung", + "migrate": "Migrieren", + "init": { + "migrateclarkson": "Migriere von Clarkson", + "migrateclarksondesc": "Wenn du bereits eine Instanz von Clarkson verwendest und die Daten migrieren möchtest, klicke hier.", + "freshinstall": "Frische Installation", + "freshinstalldesc": "Wenn du eine neue Installation von Hammond starten möchtest, klicke hier.", + "clarkson": { + "desc": "

Zuerst musst du sicherstellen, dass das Deployment von Hammond die von Clarkson verwendete MySQL Datenbank erreichen kann.

Wenn dies nicht möglich ist kannst du eine Kopie erstellen die für Hammond erreichbar ist.

Wenn das erledigt ist, füge hier den Connection String im folgenden Format ein.

Alle aus Clarkson importierten Nutzer bekommen ihren Benutzernamen als E-Mail und das Passwort wird geändert zu hammond

user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local

", + "success": "Deine Daten wurden erfolgreich von Clarkson migriert. Du wirst in kürze zur Anmeldung weitergeleitet wo du dich mit deiner E-Mail und dem passwort `hammond` anmelden kannst." + }, + "fresh": { + "setupadminuser": "Erstelle einen Administrator", + "yourpassword": "Dein Passwort", + "youremail": "Deine E-Mail-Adresse", + "yourname": "Dein Name", + "success": "Du hast dich erfolgreich registriert. Du wirst in kürze zur Anmeldung weitergeleitet und kannst Anfangen Hammond zu verwenden." + } + }, + "roles": { + "ADMIN": "Adminstrator", + "USER": "Benutzer" + }, + "profile": "Profil", + "processedon": "Bearbeitet am", + "enable": "Entsperren", + "disable": "Sperren", + "confirm": "Bestätigen", + "labelforfile": "Bezeichnung für diese Datei" + } diff --git a/src/GarageApp/locales/fr/translation.json b/src/GarageApp/locales/fr/translation.json new file mode 100644 index 00000000..df8214e9 --- /dev/null +++ b/src/GarageApp/locales/fr/translation.json @@ -0,0 +1,231 @@ +{ + "quickentry": "Pas d'entrée rapide | Entrée rapide | Entrées rapides", + "statistics": "Statistiques", + "thisweek": "Cette semaine", + "thismonth": "Ce mois", + "pastxdays": "Dernier jour | Derniers {count} jours", + "pastxmonths": "Dernier mois | Derniers {count} mois", + "thisyear": "Cette année", + "alltime": "Tout le temps", + "noattachments": "Pas de piece jointe", + "attachments": "Pièces jointes", + "choosefile": "Choose File", + "addattachment": "Ajouter une pièce jointe", + "sharedwith": "Partager avec", + "share": "Partager", + "you": "Vous", + "addfillup": "Ajouter un plein", + "createfillup": "Créer un plein", + "deletefillup": "Supprimer ce plein", + "addexpense": "Ajouter une dépense", + "createexpense": "Créer une dépense", + "deleteexpense": "Supprimer cette dépense", + "nofillups": "Pas de plein", + "transfervehicle": "Transferer le Véhicule", + "settingssaved": "Paramètres sauvegardés avec succès", + "yoursettings": "Vos paramètres", + "settings": "Paramètres", + "changepassword": "Changer votre mot de passe", + "oldpassword": "Ancien mot de passe", + "newpassword": "Nouveau mot de passe", + "repeatnewpassword": "Répéter votre nouveau mot de passe", + "passworddontmatch": "Les mots de passe ne correspondent pas", + "save": "Sauvegarder", + "supportthedeveloper": "Supporter le développeur", + "buyhimabeer": "Acheter lui un café!", + "featurerequest": "Demande de fonctionnalité", + "foundabug": "Trouvé un bug", + "currentversion": "Version actuelle", + "moreinfo": "Plus d'informations", + "currency": "Monnaie", + "distanceunit": "Unité de distance", + "dateformat": "Format de data", + "createnow": "Créer Maintenant", + "yourvehicles": "Vos Véhicules", + "menu": { + "quickentries": "Entrée rapide", + "logout": "Se déconnecter", + "import": "Importer", + "home": "Accueil", + "settings": "Paramètres", + "admin": "Admin", + "sitesettings": "Paramètres du site", + "users": "Utilisateurs", + "login": "Connexion" + }, + "enterusername": "Entrez votre nom d'utilisateur", + "enterpassword": "Entrez votre mot de passe", + "email": "Email", + "password": "Mot de passe", + "login": "connexion", + "totalexpenses": "Dépenses totales", + "fillupcost": "Coût des pleins", + "otherexpenses": "Autres dépenses", + "addvehicle": "Ajouter un Véhicule", + "editvehicle": "Editer un Véhicule", + "deletevehicle": "Supprimer un Véhicule", + "sharevehicle": "Partager un Véhicule", + "makeowner": "Changer le propriétaire", + "lastfillup": "Dernier plein", + "quickentrydesc": "Prendre une photo de la facture ou de l'écran de la pompe à essence pour créer une entrée plus tard.", + "quickentrycreatedsuccessfully": "Entrée rapide créée avec succès", + "uploadfile": "Téléverser un fichier", + "uploadphoto": "Téléverser une photo", + "details": "Détails", + "odometer": "Odomètre", + "language": "Langue", + "date": "Date", + "pastfillups": "Derniers pleins", + "fuelsubtype": "Sous-type de combustible", + "fueltype": "Type de combustible", + "quantity": "Quantité", + "gasstation": "Station service", + "fuel": { + "petrol": "Pétrol", + "diesel": "Diesel", + "cng": "CNG", + "lpg": "LPG", + "electric": "Electrique", + "ethanol": "Éthanol" + }, + "unit": { + "long": { + "litre": "Litre", + "gallon": "Gallon", + "kilowatthour": "Kilowatt Heure", + "kilogram": "Kilogram", + "usgallon": "US Gallon", + "minutes": "Minutes", + "kilometers": "Kilometres", + "miles": "Miles" + }, + "short": { + "litre": "Lt", + "gallon": "Gal", + "kilowatthour": "KwH", + "kilogram": "Kg", + "usgallon": "US Gal", + "minutes": "Mins", + "kilometers": "Km", + "miles": "Mi" + } + }, + "avgfillupqty": "Qté de plein moyen", + "avgfillupexpense": "Prix du plein moyen", + "avgfuelcost": "Prix de l'essence moyen", + "per": "{0} par {1}", + "price": "Prix", + "total": "Total", + "fulltank": "Reservoir complet", + "partialfillup": "Plein partiel", + "getafulltank": "Est-ce que vous avez rempli tout votre reservoir?", + "tankpartialfull": "Le quel traquez-vous?", + "by": "Par", + "expenses": "Dépenses", + "expensetype": "Type de dépense", + "noexpenses": "Pas de dépense", + "download": "Télécharger", + "title": "Titre", + "name": "Nom", + "delete": "Supprimer", + "importdata": "Importer des données dans Hammond", + "importdatadesc": "Choisissez une option pour importer des données dans Hammond", + "import": "Importer", + "importcsv": "Si vous utilisiez {name} pour stocker les données de vos véhicules, exportez les données en format CSV depuis {name} et cliquez ici pour importer.", + "importgeneric": "Importation de plein générique", + "importgenericdesc": "Importation de plein avec un SVC.", + "choosecsv": "Choisir un CSV", + "choosephoto": "Choisir une Photo", + "importsuccessfull": "Données importée avec succès", + "importerror": "Il y a eu un problème lors de l'importation. Veuillez regarder le message d'erreur", + "importfrom": "Importer depuis {0}", + "stepstoimport": "Étapes pour importer des données depuis {name}", + "choosecsvimport": "Choisissez le fichier CSV de {name} et appuyez sur le bouton pour importer.", + "choosedatafile": "Choisissez le fichier CSV et appuyez sur le bouton pour importer.", + "dontimportagain": "Faites attention à ne pas importer le fichier à nouveau car cela va créer des entrées dupliquées.", + "checkpointsimportcsv": "Dès que vous avez vérifié tous ces points, importez le CSV ci-dessous.", + "importhintunits": "De la même manière, make sure that the Fuel Unit and Fuel Type are correctly set in the Vehicle.", + "importhintcurrdist": "Soyez sûre que la Monnaie et l'Unité de distance sont mises correctement dans Hammond. L'importation ne detectera pas automatiquement la Monnaie du fichier mais utilisera les valeurs de l'utilisateur.", + "importhintnickname": "Soyez sûre que le nom du véhicule dans Hammon est exactement le même que le nom dans Fuelly, sinon, l'importation ne fonctionnera pas.", + "importhintvehiclecreated": "Soyez sûre d'avoir déjà créé le véhicule dans la plate-forme Hammond.", + "importhintcreatecsv": "Exportez vos données depuis {name} en format CSV. Les étapes pour faire ceci peuvent être trouvées", + "importgenerichintdata": "Les données doivent être au format CSV.", + "here": "ici", + "unprocessedquickentries": "Vous avez 1 entrée rapide en attente d'être traîtée. | Vous avez {0} entrée rapide en attente d'être traîtée.", + "show": "montrer", + "loginerror": "Il y a eu une erreur lors de la connexion a votre compte: {msg}", + "showunprocessed": "Montrer seulement les non-traîtées", + "unprocessed": "non-traîtée", + "sitesettingdesc": "Mettre à jour les paramètres du site. Ces valeurs seront utilisées par défaut pour les nouveaux utilisateurs.", + "settingdesc": "Ces valeurs seront utilisées par défaut lorsque vous créez un nouveau plein ou une nouvelle dépense.", + "areyousure": "Êtes-vous sûre de vouloir faire ceci?", + "adduser": "Ajouter un utilisateur", + "usercreatedsuccessfully": "Utilisateur créé avec succès", + "userdisabledsuccessfully": "Utilisateur désactivé avec succès", + "userenabledsuccessfully": "Utilisateur activé avec succès", + "role": "Rôle", + "created": "Créé", + "createnewuser": "Créer un nouvel utilisateur", + "cancel": "Annuler", + "novehicles": "Il semble que vous n'avez pas encore créé de véhicule dans le système pour le moment. Commencez par créer une entrée pour un des véhicule que vous voulez traquer.", + "processed": "Marquer en tant que traîté", + "notfound": "Non Trouvé", + "timeout": "La page a expiré lors du chargement. Êtes-vous sûre d'être toujours connecté à Internet?", + "clicktoselect": "Cliquer pour sélectionner...", + "expenseby": "Dépense par", + "selectvehicle": "Selectionner un véhicule", + "expensedate": "Date de la dépense", + "totalamountpaid": "Montant payé total", + "fillmoredetails": "Entrer plus de détails", + "markquickentryprocessed": "Marquer l'entrée rapide séléctionnée en tant que traîtée", + "referquickentry": "Faire référence à une entrée rapide", + "deletequickentry": "Ceci va supprimer l'entrée rapide. Cette action ne peut pas être annulée. Êtes-vous sûre?", + "fuelunit": "Unité de combustible", + "fillingstation": "Nom de la station service", + "comments": "Commentaires", + "missfillupbefore": "Est-ce que vous avez manqué un plein avant celui-ci?", + "missedfillup": "Plein manqué", + "fillupdate": "Date du plein", + "fillupsavedsuccessfully": "Plein sauvegardé avec succès", + "expensesavedsuccessfully": "Dépense sauvegardé avec succès", + "vehiclesavedsuccessfully": "Véhicule sauvegardé avec succès", + "settingssavedsuccessfully": "Paramètres sauvegardés avec succès", + "back": "Retour", + "nickname": "Surnom", + "registration": "Immatriculation", + "createvehicle": "Créer un Véhicule", + "make": "Marque", + "model": "Modèle", + "yearmanufacture": "Année de production", + "enginesize": "Taille du moteur (en chevaux)", + "mysqlconnstr": "Chaîne de caractère pour la connexion MySQL", + "testconn": "Tester la Connexion", + "migrate": "Migrer", + "init": { + "migrateclarkson": "Migrer depuis Clarkson", + "migrateclarksondesc": "Si vous avez un déploiement Clarkson existant et que vous souhaitez migrer vos données à partir de celui-ci, appuyez sur le bouton suivant.", + "freshinstall": "Nouvelle Installation", + "freshinstalldesc": "Si vous voulez une nouvelle installation de Hammond, appuyez sur le bouton suivant.", + "clarkson": { + "desc": "

Vous devez vous assurer que ce déploiement de Hammond peut accéder à la base de données MySQL utilisée par Clarkson.

Si ce n'est pas directement possible, vous pouvez faire une copie de cette base de données autre part qui est accessible à partir de cette instance.

Une fois cela fait, entrez la chaîne de connexion à l'instance MySQL au format suivant.

Tous les utilisateurs importés de Clarkson auront leur nom d'utilisateur comme e-mail dans la base de données Clarkson et le mot de passe défini sur hammond

user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local

", + "success": "Nous avons migré avec succès les données depuis Clarkson. Vous serez bientôt redirigé vers l'écran de connexion où vous pourrez vous connecter en utilisant votre adresse e-mail et votre mot de passe existants : hammond" + }, + "fresh": { + "setupadminuser": "Configurer le compte administrateur", + "yourpassword": "Votre Mot de passe", + "youremail": "Votre Email", + "yourname": "Votre Nom", + "success": "Vous avez été inscrit avec succès. Vous allez être redirigé vers la page de connexion très bientôt, vous pourrez vous connecter et commencer à utiliser le système." + } + }, + "roles": { + "ADMIN": "ADMIN", + "USER": "USER" + }, + "profile": "Profile", + "processedon": "Traîté le", + "enable": "Activer", + "disable": "Désactiver", + "confirm": "Continuer", + "labelforfile": "Label pour ce fichier" +} diff --git a/src/GarageApp/locales/hu/translation.json b/src/GarageApp/locales/hu/translation.json new file mode 100644 index 00000000..dfd2a849 --- /dev/null +++ b/src/GarageApp/locales/hu/translation.json @@ -0,0 +1,231 @@ +{ + "quickentry": "Nincs gyors bejegyzés | Gyors bejegyzés | Gyors bejegyzések", + "statistics": "Statisztikák", + "thisweek": "Ez a hét", + "thismonth": "Ez a hónap", + "pastxdays": "Elmúlt 1 nap | Elmúlt {count} nap", + "pastxmonths": "Elmúlt 1 hónap | Past {count} hónap", + "thisyear": "Ez az év", + "alltime": "Összes", + "noattachments": "Nincs csatolmány eddig", + "attachments": "Csatolmányok", + "choosefile": "Válassz fájlt", + "addattachment": "Csatolmány hozzáadása", + "sharedwith": "Megosztva vele", + "share": "Megosztás", + "you": "Te", + "addfillup": "Tankolás hozzáadása", + "createfillup": "Tankolás létrehozása", + "deletefillup": "Tankolás törlése", + "addexpense": "Kiadás hozzáadása", + "createexpense": "Kiadás létrehozása", + "deleteexpense": "Kiads törlése", + "nofillups": "Nincs tankolás eddig", + "transfervehicle": "Jármű átruházása", + "settingssaved": "Beállítások sikeresen mentve", + "yoursettings": "Beállításaid", + "settings": "Beállítások", + "changepassword": "Jelszó megváltoztatása", + "oldpassword": "Régi jelszó", + "newpassword": "Új jelszó", + "repeatnewpassword": "Jelszó mégegyszer", + "passworddontmatch": "Jelszavak nem egyeznek", + "save": "Mentés", + "supportthedeveloper": "Támogsad a fejlesztőt", + "buyhimabeer": "Vegyél neki egy sört!", + "featurerequest": "Funkció kérése", + "foundabug": "Hibát találtam", + "currentversion": "Aktuális verzió", + "moreinfo": "Több információ", + "currency": "Pénznem", + "distanceunit": "Távolság mértékegysége", + "dateformat": "Dátum formátuma", + "createnow": "Létrehozás most", + "yourvehicles": "Járműveid", + "menu": { + "quickentries": "Gyors bejegyzések", + "logout": "Kijelentkezés", + "import": "Betöltés", + "home": "Kezdőlap", + "settings": "Beállítások", + "admin": "Adminisztrátor", + "sitesettings": "Oldal beállításai", + "users": "Felhasználók", + "login": "Bejelentkezés" + }, + "enterusername": "Írd be a felhasználó neved", + "enterpassword": "Írd be a jelszavad", + "email": "Email", + "password": "Jelszó", + "login": "bejelentkezés", + "totalexpenses": "Összeskiadás", + "fillupcost": "Tankolás költsége", + "otherexpenses": "Egyéb kiadás", + "addvehicle": "Jármű hozzáadása", + "editvehicle": "Jármű szerkesztése", + "deletevehicle": "Jármű törlése", + "sharevehicle": "Jármű megosztása", + "makeowner": "Tulajdonosnak jelölés", + "lastfillup": "Utolsó tankolás", + "quickentrydesc": "Csinálj képet a blokkról/számláról vagy a tankoló mérőóráról hogy fel tudd vinni később az adatokat.", + "quickentrycreatedsuccessfully": "Gyors bejegyzés sikeresen létrehozva", + "uploadfile": "Fájl feltöltése", + "uploadphoto": "Fotó feltöltése", + "details": "Részletek", + "odometer": "Kilóméter óra", + "language": "Nyelv", + "date": "Dátum", + "pastfillups": "Előző tankolások", + "fuelsubtype": "Üzemanyag altípus", + "fueltype": "Üzemanyag típus", + "quantity": "Mennyiség", + "gasstation": "Benzinkút", + "fuel": { + "petrol": "Benzin", + "diesel": "Dízel", + "cng": "CNG", + "lpg": "LPG", + "electric": "Elektromos", + "ethanol": "Etanol" + }, + "unit": { + "long": { + "litre": "Liter", + "gallon": "Gallon", + "kilowatthour": "Kilowattóra", + "kilogram": "Kilogram", + "usgallon": "US Gallon", + "minutes": "Perc", + "kilometers": "Kilóméter", + "miles": "Mérföld" + }, + "short": { + "litre": "L", + "gallon": "Gal", + "kilowatthour": "KwH", + "kilogram": "Kg", + "usgallon": "US Gal", + "minutes": "P", + "kilometers": "Km", + "miles": "Mérf" + } + }, + "avgfillupqty": "Átl. tankolási menny.", + "avgfillupexpense": "Átl. tankolási kiadás", + "avgfuelcost": "Átl. üzemanyag ár", + "per": "{0} per {1}", + "price": "Ár", + "total": "Összesen", + "fulltank": "Tele tank", + "partialfillup": "Részleges tankolás", + "getafulltank": "Teletankoltál?", + "tankpartialfull": "Which do you track?", + "by": "által", + "expenses": "Kiadások", + "expensetype": "Kiadás típusa", + "noexpenses": "Nincs kiadás eddig", + "download": "Letöltés", + "title": "Címe", + "name": "Neve", + "delete": "Törlés", + "importdata": "Adata betöltésa Hammondba", + "importdatadesc": "Válassz az alábbi lehetőségek közül, hogy betöltsd az adatokat", + "import": "Betöltés", + "importcsv": "Ha már használtad {name}-t hogy tárold a jármű adatait, exportáld CSV-be {name}-ból/ből majd kattints it a Betöltésre.", + "importgeneric": "Általános tanoklások betöltése", + "importgenericdesc": "Tankolás CSV betöltés.", + "choosecsv": "CSV választása", + "choosephoto": "Fotó választása", + "importsuccessfull": "Adat sikeresen betöltve", + "importerror": "Betöltés közben hiba lépett fel. Kérem ellenőrizze a hibaüzenetet.", + "importfrom": "Betöltés innen: {0}", + "stepstoimport": "Lépések az adat betöltéséhez {name}-ból/ből", + "choosecsvimport": "Válassza a/az {name} CSV-t és nyomja meg a betöltés gombot.", + "choosedatafile": "Válassza a CSV fájlt majd nyomja meg az importálás gombot.", + "dontimportagain": "Legyen biztos benne, hogy nem importálja újra ugyan azt a fájlt. Dublikált bejegyzéseket eredményez.", + "checkpointsimportcsv": "Amikor végzett az összes ponttal, töltse be a CSV-t lejjebb.", + "importhintunits": "Hasonlóan, legyen biztos benne, hogy az Üzemanyag egysége és az Üzemanyag Típúsa helyesek a Jármű beállításaiban.", + "importhintcurrdist": "Ellenőrizze, hogy a Pénznem és a Távolság mértékegysége be lettek állítva a Hammond-ban. A betöltés automatikusan nem ismeri fel a bejegyzések Pénznemét, helyette a felhaszáló alapértlemzett beállítása lesz alkalmazva.", + "importhintnickname": "Ellenőrizze, hogy a Jármű neve Hammond-ban megegyezik a Fuelly CSV-ben használttal vagy a betöltés nem fog működni.", + "importhintvehiclecreated": "Ellenőrizze, hogy a Járművek már léteznek a Hammond rendszerében.", + "importhintcreatecsv": "Mentse adatát {name}-ból/ből CSV formátumba. Steps to do that can be found", + "importgenerichintdata": "Az adatnak CSV formátumban kell lennie.", + "here": "itt", + "unprocessedquickentries": "Egy gyors bejegyzés feldolgozva. | {0} gyorsbejegyzés vár feldolgozásra.", + "show": "Mutasd", + "loginerror": "Hiba történt a bejelentkezés során. {msg}", + "showunprocessed": "Csak nem feldolgozottak", + "unprocessed": "feldolgozatlan", + "sitesettingdesc": "Oldal beállításainak módosítása. Ezek lesznek használva új felhazsnálók létrehozásakor.", + "settingdesc": "Ezek lesznak alkalmazva alapértelmezett értékként, tankolásnál vagy kiadásnál.", + "areyousure": "Biztos hogy ezt akarja?", + "adduser": "Felhasználó hozzáadása", + "usercreatedsuccessfully": "Felhasználó sikeresen létrehozva", + "userdisabledsuccessfully": "Felhasználó sikeresen letiltva", + "userenabledsuccessfully": "Felhasználó sikeresen engedélyezve", + "role": "Szerepkör", + "created": "Létrehozva", + "createnewuser": "Új felhasználó létrehozása", + "cancel": "Mégsem", + "novehicles": "Eddig nem hozott létre járművet a rendszerében. Hozzon létre egyet amelynek adatait követni szeretné.", + "processed": "Megjelölés feldolgozottnak", + "notfound": "Nem talált", + "timeout": "A lap túl sokáig nem töltött be. Biztos benne, hogy még mindig van \ninternet elérése?", + "clicktoselect": "Válasszon...", + "expenseby": "Kiadások ez szerint", + "selectvehicle": "Válassz járművet", + "expensedate": "Kiadás dátuma", + "totalamountpaid": "Összes kifizetés", + "fillmoredetails": "Több részlet kitöltése", + "markquickentryprocessed": "Kiválasztott gyorsbejegyzések megjelölése feldolgozottként", + "referquickentry": "Gzorsbejegzy;s referencia", + "deletequickentry": "Ez kitörli ezt a gyorsbejegyzést. Ez a lépés visszafordíthatatlan. Biztos benne?", + "fuelunit": "Üzemenyag egység", + "fillingstation": "Benzinkút neve", + "comments": "Komment", + "missfillupbefore": "Nem töltötte ki az előző tankolást?", + "missedfillup": "Kihagyott tankolás", + "fillupdate": "Tankolás dátuma", + "fillupsavedsuccessfully": "Tankolás sikeresen mentve", + "expensesavedsuccessfully": "Kiadás sikeresen mentve", + "vehiclesavedsuccessfully": "Jármű sikeresen mentve", + "settingssavedsuccessfully": "Beállítások sikeresen mentve", + "back": "Vissza", + "nickname": "Becenév", + "registration": "Rendszám", + "createvehicle": "Jármű létrehozása", + "make": "Gyártó", + "model": "Model", + "yearmanufacture": "Gyártás éve", + "enginesize": "Motorméret (cm3)", + "mysqlconnstr": "Mysql Connection String", + "testconn": "Kapcsolat tesztelése", + "migrate": "Migrálás", + "init": { + "migrateclarkson": "Migrálás Clarkson-ból", + "migrateclarksondesc": "Ha van egy meglévő Clarkson telepítése, és szeretné adatait átmozgatni onnan, nyomja meg a következő gombot.", + "freshinstall": "Tiszta telepítés", + "freshinstalldesc": "Ha szeretné a Hammond tiszta telepítését, nyomja meg a következő gombot.", + "clarkson": { + "desc": "

Biztosítania kell, hogy ez a Hammond telepítés el tudja érni a Clarkson által készített MySQL adatbázist.

Ha ez nem megoldható, készíthet egy másolatot az adatbázisról, és elhelyzheti olyan helyre amely elérhető innen.

Amikor ezzel kész van, Állítsa me a connection String-et a MySQL-hez a következő formátumban.

Az összes Clarkson-ból migrált felhasználónak az e-mail címük lesz a felhasználó nevük, és a jelszavuk hammond lesz

user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local

", + "success": "Sikeresen migráltuk az adatot Clarkson-ból. Hamarosan át lesz irányítva a bejelentkezési oldalra, ahol használhatja a meglévő fiókját, és jelszavát: hammond" + }, + "fresh": { + "setupadminuser": "Adminisztrátor felhasználó beállítása", + "yourpassword": "Ön jelszava", + "youremail": "Ön e-mailcíme", + "yourname": "Ön neve", + "success": "Ön sikeresen regisztrált. Át lesz irányítva a bejelentkezési képernyőre, ahol be tud jelentkezni és elkezdheti használni a rendszert." + } + }, + "roles": { + "ADMIN": "ADMIN", + "USER": "FELHASZNÁLÓ" + }, + "profile": "Profil", + "processedon": "Felfdolgozva ekkor", + "enable": "Engedélyezve", + "disable": "Letiltva", + "confirm": "Megerősítés", + "labelforfile": "Címke ehheza fájlhoz" +} \ No newline at end of file diff --git a/src/GarageApp/locales/index.tsx b/src/GarageApp/locales/index.tsx index 061cdda2..b94c51ae 100644 --- a/src/GarageApp/locales/index.tsx +++ b/src/GarageApp/locales/index.tsx @@ -1,7 +1,10 @@ export const LANGUAGES = [ - { label: "Spanish", code: "es" }, - { label: "English", code: "en" }, - { label: "Italian", code: "it" }, - { label: "Danish", code: "da" }, + { label: "english | english", code: "en" }, + { label: "danish | dansk", code: "da" }, + { label: "german | deutch", code: "de" }, + { label: "frensh | français", code: "fr" }, + { label: "hungarian | magyar", code: "hu" }, + { label: "slovenian | slovenski", code: "sl" }, + { label: "Chinese (simplified) | 简体中文", code: "zh" }, ]; diff --git a/src/GarageApp/locales/sl/translation.json b/src/GarageApp/locales/sl/translation.json new file mode 100644 index 00000000..541420c7 --- /dev/null +++ b/src/GarageApp/locales/sl/translation.json @@ -0,0 +1,231 @@ +{ + "quickentry": "Ni hitrih vnosov | Hiter vnos | Hitri vnosi", + "statistics": "Statistika", + "thisweek": "Ta teden", + "thismonth": "Ta mesec", + "pastxdays": "Zadnji dan | Zadnjih {count} dni", + "pastxmonths": "Zadnji mesec | Zadnji {count} mesecev", + "thisyear": "To leto", + "alltime": "Celoten čas", + "noattachments": "Zaenkrat še ni priponk", + "attachments": "Priloge", + "choosefile": "Izberi datoteko", + "addattachment": "Dodaj prilogo", + "sharedwith": "V skupni rabi z", + "share": "Deli", + "you": "Ti", + "addfillup": "Dodaj polnjenje", + "createfillup": "Ustvarite polnjenje", + "deletefillup": "Izbriši to polnjenje", + "addexpense": "Dodaj strošek", + "createexpense": "Ustvari strošek", + "deleteexpense": "Izbriši ta strošek", + "nofillups": "Zaenkrat še brez polnjenja", + "transfervehicle": "Prevozno sredstvo", + "settingssaved": "Nastavitve so uspešno shranjene", + "yoursettings": "Vaše nastavitve", + "settings": "Nastavitve", + "changepassword": "Spremeni geslo", + "oldpassword": "Staro geslo", + "newpassword": "Novo geslo", + "repeatnewpassword": "Ponovite novo geslo", + "passworddontmatch": "Vrednosti gesel se ne ujemajo", + "save": "Shrani", + "supportthedeveloper": "Podprite razvijalca", + "buyhimabeer": "Kupi mu pivo!", + "featurerequest": "Nova funkcionalnost", + "foundabug": "Našel sem hrošča", + "currentversion": "Trenutna verzija", + "moreinfo": "Več informacij", + "currency": "Valuta", + "distanceunit": "Enota razdalje", + "dateformat": "Format datuma", + "createnow": "Ustvari zdaj", + "yourvehicles": "Vaša vozila", + "menu": { + "quickentries": "Hitri vnosi", + "logout": "Odjava", + "import": "Uvoz", + "home": "Domov", + "settings": "Nastavitve", + "admin": "Skrbnik", + "sitesettings": "Nastavitve spletnega mesta", + "users": "Uporabniki", + "login": "Vpiši se" + }, + "enterusername": "Vnesite svoje uporabniško ime", + "enterpassword": "Vnesite vaše geslo", + "email": "E-naslov", + "password": "Geslo", + "login": "Vpiši se", + "totalexpenses": "Skupni stroški", + "fillupcost": "Stroški polnjenja", + "otherexpenses": "Drugi stroški", + "addvehicle": "Dodaj vozilo", + "editvehicle": "Uredi vozilo", + "deletevehicle": "Izbriši vozilo", + "sharevehicle": "Deli vozilo", + "makeowner": "Postani lastnik", + "lastfillup": "Zadnje polnjenje", + "quickentrydesc": "Posnemite sliko računa ali zaslona črpalke za gorivo, da ju lahko vnesete pozneje.", + "quickentrycreatedsuccessfully": "Hitri vnos je bil uspešno ustvarjen", + "uploadfile": "Naloži datoteko", + "uploadphoto": "Naloži fotografijo", + "details": "Podrobnosti", + "odometer": "Odometer", + "language": "Jezik", + "date": "Datum", + "pastfillups": "Prejšnja polnjenja", + "fuelsubtype": "Podvrsta goriva", + "fueltype": "Vrsta goriva", + "quantity": "Količina", + "gasstation": "Bencinska črpalka", + "fuel": { + "petrol": "Bencin", + "diesel": "Dizelsko gorivo", + "cng": "CNG", + "lpg": "LPG", + "electric": "Elektrika", + "ethanol": "Etanol" + }, + "unit": { + "long": { + "litre": "Liter", + "gallon": "Galon", + "kilowatthour": "Kilovatna ura", + "kilogram": "Kilogram", + "usgallon": "Ameriška galona", + "minutes": "Minute", + "kilometers": "Kilometri", + "miles": "Milje" + }, + "short": { + "litre": "Lit", + "gallon": "Gal", + "kilowatthour": "KwH", + "kilogram": "Kg", + "usgallon": "US Gal", + "minutes": "Min", + "kilometers": "Km", + "miles": "Mi" + } + }, + "avgfillupqty": "Povprečna količina polnjenja", + "avgfillupexpense": "Povprečni stroški polnjenja", + "avgfuelcost": "Povprečni strošek goriva", + "per": "{0} na {1}", + "price": "Cena", + "total": "Skupaj", + "fulltank": "Rezervoar poln", + "partialfillup": "Delno polnjenje", + "getafulltank": "Ste dobili poln rezervoar?", + "tankpartialfull": "Kateremu sledite?", + "by": "Od", + "expenses": "Stroški", + "expensetype": "Vrsta stroška", + "noexpenses": "Zaenkrat ni bilo stroškov", + "download": "Prenesi", + "title": "Naslov", + "name": "Ime", + "delete": "Izbriši", + "importdata": "Uvozite podatke v Hammond", + "importdatadesc": "Za uvoz podatkov v Hammond izberite med naslednjimi možnostmi", + "import": "Uvozi", + "importcsv": "Če ste za shranjevanje podatkov o vozilu uporabljali {name}, izvozite datoteko CSV iz {name} in kliknite tukaj za uvoz.", + "importgeneric": "Generični uvoz polnjenja", + "importgenericdesc": "CSV uvoz poljenja.", + "choosecsv": "Izberite CSV", + "choosephoto": "Izberite fotografijo", + "importsuccessfull": "Podatki so bili uspešno uvoženi", + "importerror": "Pri uvozu datoteke je prišlo do težave. ", + "importfrom": "Uvoz iz {0}", + "stepstoimport": "Koraki za uvoz podatkov iz {name}", + "choosecsvimport": "Izberite {name} CSV in pritisnite gumb za uvoz.", + "choosedatafile": "Izberite datoteko CSV in pritisnite gumb za uvoz.", + "dontimportagain": "Prepričajte se, da datoteke ne uvozite znova, ker boste s tem ustvarili ponavljajoče se vnose.", + "checkpointsimportcsv": "Ko preverite vse te točke, preprosto uvozite spodnji CSV.", + "importhintunits": "Podobno se prepričajte, da sta Enota za gorivo in Vrsta goriva pravilno nameščeni pod Vozilo.", + "importhintcurrdist": "Prepričajte se, da sta Valuta in Enota razdalje pri Hammondu uporabniku pravilno nastavljeni.", + "importhintnickname": "Prepričajte se, da je vzdevek vozila v Hammondu popolnoma enak imenu v CSV datoteki polnjenj, sicer uvoz ne bo deloval.", + "importhintvehiclecreated": "Prepričajte se, da ste že ustvarili vozila na platformi Hammond.", + "importhintcreatecsv": "Izvozite svoje podatke iz {name} v formatu CSV. ", + "importgenerichintdata": "Podatki morajo biti v formatu CSV.", + "here": "tukaj", + "unprocessedquickentries": "Za obdelavo imate en hiter vnos. | Za obdelavo imate {0} hitrih vnosov.", + "show": "Prikaži", + "loginerror": "Pri prijavi v vaš račun je prišlo do napake. ", + "showunprocessed": "Pokaži samo neobdelano", + "unprocessed": "neobdelano", + "sitesettingdesc": "Posodobite nastavitve na ravni aplikacije. Te bodo uporabljene kot privzete vrednosti za nove uporabnike.", + "settingdesc": "Te bodo uporabljene kot privzete vrednosti vsakič, ko ustvarite novo polnjenje ali strošek.", + "areyousure": "Ste prepričani, da želite to narediti?", + "adduser": "Dodaj uporabnika", + "usercreatedsuccessfully": "Uporabnik je bil uspešno ustvarjen", + "userdisabledsuccessfully": "Uporabnik uspešno onemogočen", + "userenabledsuccessfully": "Uporabnik je uspešno omogočen", + "role": "Vloga", + "created": "Ustvarjeno", + "createnewuser": "Ustvari novega uporabnika", + "cancel": "Prekliči", + "novehicles": "Videti je, da še niste ustvarili vozila v sistemu. ", + "processed": "Označi obdelano", + "notfound": "Ni najdeno", + "timeout": "Med nalaganjem strani je potekla časovna omejitev. ", + "clicktoselect": "Kliknite za izbiro ...", + "expenseby": "Stroški po", + "selectvehicle": "Izberite vozilo", + "expensedate": "Datum izdatka", + "totalamountpaid": "Skupni plačani znesek", + "fillmoredetails": "Izpolnite več podrobnosti", + "markquickentryprocessed": "Označi izbrani hitri vnos kot obdelan", + "referquickentry": "Oglejte si hiter vnos", + "deletequickentry": "S tem boste izbrisali ta hitri vnos. ", + "fuelunit": "Enota za gorivo", + "fillingstation": "Ime bencinske postaje", + "comments": "Komentarji", + "missfillupbefore": "Ste pred tem zamudili vnos zapolnitve?", + "missedfillup": "Zamujeno polnjenje", + "fillupdate": "Datum polnjenja", + "fillupsavedsuccessfully": "Polnjenje je bil uspešno shranjeno", + "expensesavedsuccessfully": "Stroški so uspešno shranjeni", + "vehiclesavedsuccessfully": "Vozilo je uspešno shranjeno", + "settingssavedsuccessfully": "Nastavitve so uspešno shranjene", + "back": "Nazaj", + "nickname": "Vzdevek", + "registration": "Registracija", + "createvehicle": "Ustvarite vozilo", + "make": "Znamka / Podjetje", + "model": "Model", + "yearmanufacture": "Leto izdelave", + "enginesize": "Prostornina motorja (v cc)", + "mysqlconnstr": "Niz povezave Mysql", + "testconn": "Testna povezava", + "migrate": "Preseli", + "init": { + "migrateclarkson": "Selitev iz Clarksona", + "migrateclarksondesc": "Če imate obstoječo Clarkson namestitev in želite iz nje preseliti svoje podatke, pritisnite naslednji gumb.", + "freshinstall": "Sveža namestitev", + "freshinstalldesc": "Če želite novo namestitev Hammonda, pritisnite naslednji gumb.", + "clarkson": { + "desc": "

Zagotoviti morate, da lahko ta uvedba Hammonda dostopa do baze podatkov MySQL, ki jo uporablja Clarkson.

Če to ni neposredno mogoče, lahko naredite kopijo te zbirke podatkov nekje, kjer je dostopna iz tega primerka.

Ko je to storjeno, vnesite povezovalni niz v primerek MySQL v naslednji obliki.

Vsi uporabniki, uvoženi iz Clarksona, bodo imeli svoje uporabniško ime kot e-pošto v bazi podatkov Clarkson in geslo nastavljeno nahammond

uporabnik:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4

", + "success": "Uspešno smo prenesli podatke iz Clarksona. " + }, + "fresh": { + "setupadminuser": "Nastavitev skrbniških uporabnikov", + "yourpassword": "Vaše geslo", + "youremail": "Vaš e-poštni naslov", + "yourname": "Vaše ime", + "success": "Uspešno ste se registrirali. " + } + }, + "roles": { + "ADMIN": "SKRBNIK", + "USER": "UPORABNIK" + }, + "profile": "Profil", + "processedon": "Obdelano dne", + "enable": "Omogoči", + "disable": "Onemogoči", + "confirm": "Kar daj", + "labelforfile": "Oznaka za to datoteko" +} \ No newline at end of file diff --git a/src/GarageApp/locales/zh/translation.json b/src/GarageApp/locales/zh/translation.json new file mode 100644 index 00000000..3a565af7 --- /dev/null +++ b/src/GarageApp/locales/zh/translation.json @@ -0,0 +1,231 @@ +{ + "quickentry": "没有速记 | 速记 | 速记", + "statistics": "统计数据", + "thisweek": "本周", + "thismonth": "本月", + "pastxdays": "过去一天 | 过去 {count} 天", + "pastxmonths": "过去一个月 | 过去 {count} 个月", + "thisyear": "今年", + "alltime": "所有时间", + "noattachments": "目前还没有附件", + "attachments": "附件", + "choosefile": "选择文件", + "addattachment": "添加附件", + "sharedwith": "共享给", + "share": "分享", + "you": "您", + "addfillup": "添加加注记录", + "createfillup": "创建加注记录", + "deletefillup": "删除此加注记录", + "addexpense": "添加支出", + "createexpense": "创建支出记录", + "deleteexpense": "删除此支出", + "nofillups": "目前还没有加注记录", + "transfervehicle": "转让车辆", + "settingssaved": "设置保存成功", + "yoursettings": "您的设置", + "settings": "设置", + "changepassword": "更改密码", + "oldpassword": "旧密码", + "newpassword": "新密码", + "repeatnewpassword": "重复新密码", + "passworddontmatch": "密码不匹配", + "save": "保存", + "supportthedeveloper": "支持开发者", + "buyhimabeer": "请他喝杯啤酒!", + "featurerequest": "功能请求", + "foundabug": "发现了错误", + "currentversion": "当前版本", + "moreinfo": "更多信息", + "currency": "货币", + "distanceunit": "长度单位", + "dateformat": "日期格式", + "createnow": "立即创建", + "yourvehicles": "您的车辆", + "menu": { + "quickentries": "速记", + "logout": "退出登录", + "import": "导入", + "home": "主页", + "settings": "设置", + "admin": "管理", + "sitesettings": "全局设置", + "users": "用户", + "login": "登录" + }, + "enterusername": "输入您的用户名", + "enterpassword": "输入您的密码", + "email": "邮箱", + "password": "密码", + "login": "登录", + "totalexpenses": "总支出", + "fillupcost": "加注花费", + "otherexpenses": "其它支出", + "addvehicle": "添加车辆", + "editvehicle": "编辑车辆", + "deletevehicle": "删除车辆", + "sharevehicle": "共享车辆", + "makeowner": "设为车主", + "lastfillup": "上次加注记录", + "quickentrydesc": "拍摄发票或加油机显示屏的照片,以便稍后进行记录。", + "quickentrycreatedsuccessfully": "速记创建成功", + "uploadfile": "上传文件", + "uploadphoto": "上传照片", + "details": "详情", + "odometer": "里程表", + "language": "语言", + "date": "日期", + "pastfillups": "历史加注记录", + "fuelsubtype": "燃油型号", + "fueltype": "燃油类型", + "quantity": "加注量", + "gasstation": "加油站", + "fuel": { + "petrol": "汽油", + "diesel": "柴油", + "cng": "压缩天然气", + "lpg": "液化石油气", + "electric": "电力", + "ethanol": "乙醇" + }, + "unit": { + "long": { + "litre": "升", + "gallon": "加仑", + "kilowatthour": "千瓦时", + "kilogram": "千克", + "usgallon": "美制加仑", + "minutes": "分钟", + "kilometers": "千米", + "miles": "英里" + }, + "short": { + "litre": "升", + "gallon": "加仑", + "kilowatthour": "度", + "kilogram": "千克", + "usgallon": "美加仑", + "minutes": "分", + "kilometers": "千米", + "miles": "英里" + } + }, + "avgfillupqty": "平均加注量", + "avgfillupexpense": "平均加注费用", + "avgfuelcost": "平均燃油成本", + "per": "每{1}的{0}", + "price": "价格", + "total": "总价", + "fulltank": "加满油箱", + "partialfillup": "部分加注", + "getafulltank": "您是否加满油箱?", + "tankpartialfull": "油箱状态如何标记?", + "by": "来自", + "expenses": "支出记录", + "expensetype": "支出类型", + "noexpenses": "目前还没有支出记录", + "download": "下载", + "title": "文件说明", + "name": "文件名", + "delete": "删除", + "importdata": "将数据导入 Hammond", + "importdatadesc": "从以下选项中进行选择以将数据导入 Hammond", + "import": "导入", + "importcsv": "如果您此前使用 {name} 存储车辆数据,请从 {name} 导出 CSV 文件,然后单击此处进行导入。", + "importgeneric": "通用导入", + "importgenericdesc": "导入加注信息的 CSV 文件。", + "choosecsv": "选择 CSV", + "choosephoto": "选择照片", + "importsuccessfull": "数据导入成功", + "importerror": "导入文件时出现问题,请检查错误信息。", + "importfrom": "从 {0} 导入", + "stepstoimport": "从 {name} 导入数据的步骤", + "choosecsvimport": "选择 {name} 的 CSV 文件,并单击导入按钮。", + "choosedatafile": "选择 CSV 文件,然后单击导入按钮。", + "dontimportagain": "确保不要再次导入文件,因为这样会产生重复数据。", + "checkpointsimportcsv": "检查完所有这些要点后,就在下面导入 CSV。", + "importhintunits": "同样,请确保车辆中的燃油单位燃油类型设置正确。", + "importhintcurrdist": "确保在 Hammond 中正确设置了货币距离单位。导入时不会从文件中自动检测货币,而是直接使用用户设置的货币。", + "importhintnickname": "确保 Hammond 中的车辆昵称与 CSV 中的名称完全相同,否则导入将不起作用。", + "importhintvehiclecreated": "确保已在 Hammond 中创建车辆。", + "importhintcreatecsv": "以 CSV 格式从 {name} 导出数据。可以在这里找到执行此操作的步骤:", + "importgenerichintdata": "数据必须为 CSV 格式。", + "here": "(文档)", + "unprocessedquickentries": "您有一条速记等待处理。 | 您有 {0} 条速记等待处理。", + "show": "显示", + "loginerror": "登录您的帐户时出错。{msg}", + "showunprocessed": "仅显示未处理的", + "unprocessed": "未处理的", + "sitesettingdesc": "更新全局设置,这些设置将作为新用户的默认值。", + "settingdesc": "当您创建新的加注或支出记录时,这些值将作为默认值使用。", + "areyousure": "您确定要这样做吗?", + "adduser": "添加用户", + "usercreatedsuccessfully": "用户创建成功", + "userdisabledsuccessfully": "用户禁用成功", + "userenabledsuccessfully": "用户启用成功", + "role": "角色", + "created": "创建", + "createnewuser": "创建新用户", + "cancel": "取消", + "novehicles": "您似乎尚未在系统中创建车辆,请先添加一辆。", + "processed": "标记为已处理", + "notfound": "未找到", + "timeout": "页面加载时超时,您确定仍连接到互联网吗?", + "clicktoselect": "单击以选择…", + "expenseby": "支出由", + "selectvehicle": "选择车辆", + "expensedate": "支出日期", + "totalamountpaid": "支付总额", + "fillmoredetails": "填写更多详细信息", + "markquickentryprocessed": "将选定的速记标记为已处理", + "referquickentry": "关联速记", + "deletequickentry": "此操作将删除这条速记。删除后无法恢复,您确定要继续吗?", + "fuelunit": "燃油单位", + "fillingstation": "加油站名称", + "comments": "注释", + "missfillupbefore": "您是否遗漏了之前的一次加注记录?", + "missedfillup": "遗漏的加注记录", + "fillupdate": "加注日期", + "fillupsavedsuccessfully": "加注记录保存成功", + "expensesavedsuccessfully": "支出记录保存成功", + "vehiclesavedsuccessfully": "车辆保存成功", + "settingssavedsuccessfully": "设置保存成功", + "back": "返回", + "nickname": "昵称", + "registration": "车牌号", + "createvehicle": "创建车辆", + "make": "品牌 / 制造商", + "model": "型号", + "yearmanufacture": "生产年份", + "enginesize": "发动机排量(单位:cc)", + "mysqlconnstr": "MySQL 连接字符串", + "testconn": "测试连接", + "migrate": "迁移", + "init": { + "migrateclarkson": "从 Clarkson 迁移", + "migrateclarksondesc": "如果您已有 Clarkson 部署,并希望从中迁移数据,请单击以下按钮。", + "freshinstall": "全新安装", + "freshinstalldesc": "如果您想全新安装 Hammond,请单击以下按钮。", + "clarkson": { + "desc": "

您需要此确保 Hammond 的部署可以访问 Clarkson 使用的 MySQL 数据库。

如果无法直接访问,可以在本实例可访问的地方复制该数据库。

<完成后,按以下格式输入与 MySQL 实例的连接字符串。/p>

所有从 Clarkson 导入的用户将使用原邮箱作为用户名,密码设置为hammond

user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local

", + "success": "我们已成功从 Clarkson 迁移数据。您将很快被重定向到登录界面,在那里您可以使用现有的邮箱和密码(hammond)登录。" + }, + "fresh": { + "setupadminuser": "设置管理员用户", + "yourpassword": "您的密码", + "youremail": "您的邮箱", + "yourname": "您的姓名", + "success": "您已注册成功。您将很快被重定向到登录界面,登录后即可开始使用。" + } + }, + "roles": { + "ADMIN": "管理员", + "USER": "普通用户" + }, + "profile": "配置文件", + "processedon": "处理于", + "enable": "启用", + "disable": "禁用", + "confirm": "继续", + "labelforfile": "文件说明" +} diff --git a/src/GarageApp/modules/.VehicleCards.tsx.swp b/src/GarageApp/modules/.StatisticsView.tsx.swp similarity index 68% rename from src/GarageApp/modules/.VehicleCards.tsx.swp rename to src/GarageApp/modules/.StatisticsView.tsx.swp index 5588ab0d..885372eb 100644 Binary files a/src/GarageApp/modules/.VehicleCards.tsx.swp and b/src/GarageApp/modules/.StatisticsView.tsx.swp differ diff --git a/src/GarageApp/modules/MenuBar.tsx b/src/GarageApp/modules/MenuBar.tsx new file mode 100644 index 00000000..3a402d28 --- /dev/null +++ b/src/GarageApp/modules/MenuBar.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import Menu from '@mui/material/Menu'; +import MenuIcon from '@mui/icons-material/Menu'; +import Container from '@mui/material/Container'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; +import MenuItem from '@mui/material/MenuItem'; +import AdbIcon from '@mui/icons-material/Adb'; + +import MenuLanguages from './MenuLanguages'; +import logo from '../logo.svg' +import reactLogo from '../react.svg' + + +//translation +import { useTranslation } from 'react-i18next'; +import { LANGUAGES } from '../locales' + +// a test to see if this will work in menu +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; + + +function GarageAppLogo() { + return ( +
+
+ Bun Logo + React Logo +
+
+

+ GarageApp +

+
+
+ ) +} +const pages = ['Home', 'Quick Entries', 'Import']; +const settings = ['Profile', 'Admin', 'Logout']; +function ResponsiveAppBar() { + //translation + const { t, i18n } = useTranslation(); + + //MaterialUI stuff + const [anchorElNav, setAnchorElNav] = React.useState(null); + const [anchorElUser, setAnchorElUser] = React.useState(null); + + const handleOpenNavMenu = (event: React.MouseEvent) => { + setAnchorElNav(event.currentTarget); + }; + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + return ( + + + + + + + + + + {pages.map((page) => ( + + {page} + + ))} + + + + + LOGO + + + {pages.map((page) => ( + + ))} + + + + + + + + + {settings.map((setting) => ( + + {setting} + + ))} + + + + + + + + + ); +} +export default ResponsiveAppBar; diff --git a/src/GarageApp/modules/MenuLanguages.tsx b/src/GarageApp/modules/MenuLanguages.tsx new file mode 100644 index 00000000..4dbd32f0 --- /dev/null +++ b/src/GarageApp/modules/MenuLanguages.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import Paper from '@mui/material/Paper'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemButton from '@mui/material/ListItemButton'; +import { LANGUAGES } from '../locales' +import { Icon } from "@iconify/react"; +import { useTranslation } from 'react-i18next'; +import Menu from '@mui/material/Menu'; + + +export default function LanguageMenu( {closeAction} ) { + const { t, i18n } = useTranslation(); + + const handleLanguageChange = (lang: any) => { + try { + i18n.changeLanguage(lang.code); + } catch (error) { + console.error(`Error changing language to ${lang.code}:`, error); + } + }; + + const [anchorElUser, setAnchorElUser] = React.useState(null); + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedIndex, setSelectedIndex] = React.useState(1); + const open = Boolean(anchorEl); + const handleClickListItem = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + + const handleLanguageSelect = ( + event: React.MouseEvent, + index: number, + ) => { + setSelectedIndex(index); + handleLanguageChange(LANGUAGES[index]); + closeAction; + setAnchorEl(null); + setAnchorElUser(null); + }; + + + const handleClose = () => { + setAnchorEl(null); + }; + + + return ( + + + + + + {LANGUAGES.map((lang, index) => + handleLanguageSelect(event, index)}> + + + + {lang.label} + + )} + + + ) +} diff --git a/src/GarageApp/modules/StatisticsView.tsx b/src/GarageApp/modules/StatisticsView.tsx new file mode 100644 index 00000000..34d792cf --- /dev/null +++ b/src/GarageApp/modules/StatisticsView.tsx @@ -0,0 +1,13 @@ +import { useTranslation, withTranslation, Trans } from 'react-i18next'; + + + + + + +export default function StatisticsView() { + const { t, i18n } = useTranslation(); + return ( +

{t ('statistics')}

+ ) +} diff --git a/src/GarageApp/modules/VehicleCards.tsx b/src/GarageApp/modules/VehicleCards.tsx index 77e37ebc..20f4ddcf 100644 --- a/src/GarageApp/modules/VehicleCards.tsx +++ b/src/GarageApp/modules/VehicleCards.tsx @@ -13,8 +13,6 @@ import IconButton, { IconButtonProps } from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { red } from '@mui/material/colors'; -import FavoriteIcon from '@mui/icons-material/Favorite'; -import ShareIcon from '@mui/icons-material/Share'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import GasStation from './icons/gasFillup' diff --git a/src/GarageApp/modules/YourVehicles.tsx b/src/GarageApp/modules/YourVehicles.tsx index a8b8b9c3..14004376 100644 --- a/src/GarageApp/modules/YourVehicles.tsx +++ b/src/GarageApp/modules/YourVehicles.tsx @@ -6,10 +6,10 @@ import Button from '@mui/material/Button'; import { useTranslation, withTranslation, Trans } from 'react-i18next'; - const response = await fetch("/GarageApp/api/vehicles"); - console.log(response) - const vehicles = await response.json(); - console.log(vehicles) +const response = await fetch("/GarageApp/api/vehicles"); +// console.log(response) +const vehicles = await response.json(); +// console.log(vehicles) export default function YourVehicleList() { const { t, i18n } = useTranslation(); // const response = await fetch("/GarageApp/api/vehicles");