diff --git a/.dev/browser/blocked-icon-sets.d.mts b/.dev/browser/blocked-icon-sets.d.mts new file mode 100644 index 0000000..ebf1fcf --- /dev/null +++ b/.dev/browser/blocked-icon-sets.d.mts @@ -0,0 +1,4 @@ +/** Whole packages that are never sampled or counted, by composer name, with the reason. */ +export const BLOCKED_PACKAGES: Readonly>; +/** Why an icon (or a whole pack) may not be sampled, or `undefined` when it may. */ +export function blockedReason(pkg: string): string | undefined; diff --git a/.dev/browser/blocked-icon-sets.mjs b/.dev/browser/blocked-icon-sets.mjs new file mode 100644 index 0000000..433800d --- /dev/null +++ b/.dev/browser/blocked-icon-sets.mjs @@ -0,0 +1,34 @@ +// Icon packs the dev fixture must never sample, count or list. The one list: +// `scripts/generate-fixture.mjs` skips each pack whole, and +// `src/core/blockedIconSets.test.ts` proves the generator drops every entry. +// +// It mirrors ichava/react-landingpage's `scripts/check-landing-data.mjs` (INTERNAL and +// check 8, react-landingpage#40). That package carries a copy of this fixture's +// `icons.json`, so the two lists must move together: a pack allowed here and blocked +// there fails the landing check on the next copy. +// +// - Metronic is licensed to Simtabi LLC for internal and client work only and may never +// appear on a public surface (decided 2026-10-26). +// - icon-sets-bundled is private to Simtabi LLC, for internal and client use only and not +// redistributable (decided 2026-10-26). Blocking the whole pack replaced the per-set +// list that blocked its GPL (icomoon), unverified and mislabelled sets. +// +// A blocked pack is excluded entirely: no sample, no asset, no entry in `packages`, and +// no share of `total_ecosystem`. The landing page counts the same way, so the two totals +// agree. + +/** Whole packages that are never sampled or counted, by composer name, with the reason. */ +export const BLOCKED_PACKAGES = Object.freeze({ + 'ichava/icon-sets-metronic': 'internal and client use only', + 'ichava/icon-sets-bundled': 'private, internal and client use only, not redistributable', +}); + +/** + * Why an icon (or a whole pack) may not be sampled, or `undefined` when it may. + * + * @param {string} pkg the composer name, e.g. `ichava/icon-sets-bundled` + * @returns {string | undefined} + */ +export function blockedReason(pkg) { + return Object.hasOwn(BLOCKED_PACKAGES, pkg) ? BLOCKED_PACKAGES[pkg] : undefined; +} diff --git a/.dev/browser/brand-bridge.mjs b/.dev/browser/brand-bridge.mjs new file mode 100644 index 0000000..f780ed9 --- /dev/null +++ b/.dev/browser/brand-bridge.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +// Regenerates the brand bridge block in src/styles/theme.css (see the comment above it). +// +// node scripts/brand-bridge.mjs rewrite the block between the markers +// node scripts/brand-bridge.mjs --check exit 1 if the block is out of date +// +// The block re-declares, on the component root, every `:root` token whose value depends +// on `--color-brand-*`, with the same expression `:root` uses. A custom property's var() +// references resolve where it is DECLARED, so a token declared only at `:root` has the +// shipped purple substituted there, and the ramp the component root writes never reaches +// it. `src/styles/brandBridge.test.ts` runs the same derivation, so a resync of +// uui-theme.css that adds a brand token fails `npm test` until this is re-run. + +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { stripCssComments } from '@ichava/js-ui-kernel/build'; +import { fileURLToPath } from 'node:url'; + +export const BEGIN = '/* brand-bridge:begin (generated by scripts/brand-bridge.mjs; do not edit by hand) */'; +export const END = '/* brand-bridge:end */'; + +/** Variables the component root writes inline; inline wins, so re-declaring them is noise. */ +export const INLINE = new Set(['--accent', '--accent-text', '--accent-soft', '--ring', '--radius']); + +const WORD = /[\w-]/; +const SPACE = /\s/; + +/** + * `name: value` declarations in a CSS fragment, comments stripped: exactly the matches of + * `/(--[\w-]+)\s*:\s*([^;{}]+);/g`, with the value trimmed, found in one pass. + * + * That regex is polynomial: `\s*` and `[^;{}]+` both take spaces, and `[\w-]+` retries + * from every dash, so a long unterminated value or a run of dashes took seconds. Here + * each position is visited a fixed number of times. Comments go through the kernel's + * linear `stripCssComments` for the same reason. + */ +export function declarations(css) { + const text = stripCssComments(css); + const n = text.length; + // wordEnd[i]: the first index >= i outside [\w-]; spaceEnd[i]: outside \s; + // stop[i]: the first index >= i holding one of ; { } (n when none). + const wordEnd = new Int32Array(n + 1); + const spaceEnd = new Int32Array(n + 1); + const stop = new Int32Array(n + 1); + wordEnd[n] = spaceEnd[n] = stop[n] = n; + for (let i = n - 1; i >= 0; i--) { + const c = text[i]; + wordEnd[i] = WORD.test(c) ? wordEnd[i + 1] : i; + spaceEnd[i] = SPACE.test(c) ? spaceEnd[i + 1] : i; + stop[i] = c === ';' || c === '{' || c === '}' ? i : stop[i + 1]; + } + const out = []; + for (let p = 0; p + 1 < n; ) { + if (text[p] === '-' && text[p + 1] === '-') { + const nameEnd = wordEnd[p + 2]; + const colon = spaceEnd[nameEnd]; + if (nameEnd > p + 2 && text[colon] === ':') { + const end = stop[colon + 1]; + if (end < n && text[end] === ';' && end > colon + 1) { + out.push([text.slice(p, nameEnd), text.slice(colon + 1, end).trim()]); + p = end + 1; + continue; + } + } + } + p += 1; + } + return out; +} + +/** The `:root`-level declarations: Untitled UI's `@theme` block plus theme.css's `:root` block. */ +export function rootDeclarations(uuiCss, themeCss) { + const theme = uuiCss.slice(uuiCss.indexOf('@theme'), uuiCss.indexOf('@layer base')); + const open = themeCss.indexOf(':root {'); + const app = open < 0 ? '' : themeCss.slice(open, themeCss.indexOf('\n }\n', open)); + return [...declarations(theme), ...declarations(app)]; +} + +/** Every declaration that depends, directly or transitively, on `--color-brand-`. */ +export function brandDependents(decls) { + const map = new Map(decls); + const dep = new Set([...map.keys()].filter((k) => /^--color-brand-\d+$/.test(k))); + for (let changed = true; changed; ) { + changed = false; + for (const [k, v] of map) { + if (dep.has(k)) continue; + if ([...v.matchAll(/var\(\s*(--[\w-]+)/g)].some((m) => dep.has(m[1]))) { + dep.add(k); + changed = true; + } + } + } + return [...map].filter(([k]) => dep.has(k) && !/^--color-brand-\d+$/.test(k) && !INLINE.has(k)); +} + +export function renderBlock(entries) { + const body = entries.map(([k, v]) => ` ${k}: ${v};`).join('\n'); + return `${BEGIN}\n@layer base {\n :where([data-ichava-theme]) {\n${body}\n }\n}\n${END}`; +} + +export function currentBlock(themeCss) { + const a = themeCss.indexOf(BEGIN); + const b = themeCss.indexOf(END); + return a < 0 || b < 0 ? null : themeCss.slice(a, b + END.length); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); + const themePath = join(root, 'resources/js/styles/theme.css'); + // Untitled UI's theme is @ichava/js-ui-kernel/react's since react consolidation 3d (2026-11-12); + // resources/js/styles/uui-theme.css only imports it. + const uui = readFileSync(join(root, 'node_modules/@ichava/js-ui-kernel/dist/react/styles/uui-theme.css'), 'utf8'); + const themeCss = readFileSync(themePath, 'utf8'); + const entries = brandDependents(rootDeclarations(uui, themeCss)); + if (entries.length < 50) { + console.error(`::error::only ${entries.length} brand-dependent tokens found; the parse is probably broken`); + process.exit(1); + } + const want = renderBlock(entries); + const have = currentBlock(themeCss); + if (have === null) { + console.error('::error::brand-bridge markers not found in src/styles/theme.css'); + process.exit(1); + } + if (process.argv.includes('--check')) { + if (have !== want) { + console.error('::error::the brand bridge in theme.css is stale; run node scripts/brand-bridge.mjs'); + process.exit(1); + } + console.log(`brand-bridge: ok -- ${entries.length} tokens`); + } else { + writeFileSync(themePath, themeCss.replace(have, want)); + console.log(`brand-bridge: wrote ${entries.length} tokens`); + } +} diff --git a/.dev/browser/check-boundary-rules.mjs b/.dev/browser/check-boundary-rules.mjs new file mode 100644 index 0000000..2e76097 --- /dev/null +++ b/.dev/browser/check-boundary-rules.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// Proves the module boundary rules in .dependency-cruiser.cjs can fail. +// +// The rules for src/modules/ and src/state/ land before the code they govern +// (architecture.md 4.1), so on a tree with no modules they match nothing, and "no +// violations" would say nothing about whether they work. `core-no-react` once shipped +// matching nothing for exactly that reason. +// +// This plants one violation of each rule in throwaway directories, runs dependency-cruiser +// over them, and fails unless every rule fires on its plant and the allowed import (one +// module reaching another through its index.ts) fires nothing. The plants are removed +// whatever happens. + +import { execFileSync } from 'node:child_process'; +import { mkdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const A = 'src/modules/__probe_a__'; +const B = 'src/modules/__probe_b__'; +const STATE = 'src/state/__probe__.ts'; +const PLANTED = [A, B, STATE]; + +/** file -> source. Each import names an existing module or another plant. */ +const FILES = { + [`${A}/index.ts`]: `export const probe = 1;\n`, + [`${A}/domain/react.ts`]: `import type { ReactNode } from 'react';\nexport type Node = ReactNode;\n`, + [`${A}/domain/outward.ts`]: `import { service } from '../application/service';\nexport const d = service;\n`, + [`${A}/application/service.ts`]: `import { adapter } from '../infrastructure/adapter';\nexport const service = adapter;\n`, + [`${A}/infrastructure/adapter.ts`]: `import { useT } from '@/hooks/useT';\nexport const adapter = useT;\n`, + [`${B}/domain/deep.ts`]: `import { d } from '../../__probe_a__/domain/outward';\nexport const deep = d;\n`, + [`${B}/domain/through-index.ts`]: `import { probe } from '../../__probe_a__/index';\nexport const ok = probe;\n`, + [STATE]: `import { useT } from '@/hooks/useT';\nexport const s = useT;\n`, +}; + +/** rule -> the plant it must fire on. */ +const EXPECT = { + 'module-domain-is-plain-ts': `${A}/domain/react.ts`, + 'module-domain-is-innermost': `${A}/domain/outward.ts`, + 'module-application-not-outward': `${A}/application/service.ts`, + 'module-infrastructure-not-into-ui': `${A}/infrastructure/adapter.ts`, + 'modules-meet-through-index': `${B}/domain/deep.ts`, + 'state-not-into-ui': STATE, +}; +const CONTROL = `${B}/domain/through-index.ts`; + +function cleanup() { + for (const p of PLANTED) rmSync(join(root, p), { recursive: true, force: true }); + // Remove the parent directories too when the plants were all they held. + for (const dir of ['src/modules', 'src/state']) { + try { + rmdirSync(join(root, dir)); + } catch { + /* not empty, or not there */ + } + } +} + +let violations; +try { + for (const [file, source] of Object.entries(FILES)) { + mkdirSync(dirname(join(root, file)), { recursive: true }); + writeFileSync(join(root, file), source); + } + let out; + try { + out = execFileSync( + 'npx', + ['depcruise', ...PLANTED, '--config', '.dependency-cruiser.cjs', '--output-type', 'json'], + { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 }, + ); + } catch (error) { + // depcruise exits non-zero when it finds errors, which is the point here. + out = error.stdout; + } + violations = JSON.parse(out).summary.violations; +} finally { + cleanup(); +} + +const problems = []; +for (const [rule, file] of Object.entries(EXPECT)) { + if (!violations.some((v) => v.rule.name === rule && v.from === file)) { + problems.push(`${rule} did not fire on ${file}`); + } +} +const control = violations.filter((v) => v.from === CONTROL); +if (control.length) { + problems.push(`the allowed import through index.ts fired ${control.map((v) => v.rule.name).join(', ')}`); +} + +if (problems.length) { + for (const p of problems) console.error(`check-boundary-rules: ${p}`); + process.exit(1); +} +console.log(`check-boundary-rules: ok -- ${Object.keys(EXPECT).length} rules fire on their plants, the index.ts import passes`); diff --git a/.dev/browser/check-config-drift.mjs b/.dev/browser/check-config-drift.mjs new file mode 100644 index 0000000..3182490 --- /dev/null +++ b/.dev/browser/check-config-drift.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// Canonical: ichava/js-ui-kernel config/check-config-drift.mjs. Copies must be +// byte-identical, and this script checks its own copy too: list it as one of the pairs. +// +// Usage: node scripts/check-config-drift.mjs = [= ...] +// +// path in this repository, relative to the working directory +// path inside ichava/js-ui-kernel, e.g. config/tsconfig.base.json +// +// Each canonical file is fetched from raw.githubusercontent.com and compared byte for byte. +// It fails on a mismatch, on a non-200 response, on an empty body and on an empty pair +// list, so it cannot pass by comparing nothing. +// +// ICHAVA_KERNEL_REF git ref to compare against (default: main) +// ICHAVA_KERNEL_RAW raw base URL (default: https://raw.githubusercontent.com/ichava/js-ui-kernel) +import { readFile } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; + +export const DEFAULT_RAW = 'https://raw.githubusercontent.com/ichava/js-ui-kernel'; + +/** Parses `local=canonical` arguments. Throws on a malformed one. */ +export function parsePairs(args) { + return args.map((arg) => { + const eq = arg.indexOf('='); + if (eq <= 0 || eq === arg.length - 1) { + throw new Error(`expected =, got "${arg}"`); + } + return { local: arg.slice(0, eq), canonical: arg.slice(eq + 1) }; + }); +} + +/** + * Compares each pair. Returns { checked, failures }, where failures is a list of + * human-readable reasons. `fetchImpl` and `readImpl` are injectable for tests. + */ +export async function checkDrift({ + pairs, + ref = 'main', + raw = DEFAULT_RAW, + fetchImpl = globalThis.fetch, + readImpl = (path) => readFile(path), +}) { + const failures = []; + if (pairs.length === 0) { + failures.push('no files to compare: pass at least one = pair'); + return { checked: 0, failures }; + } + + let checked = 0; + for (const { local, canonical } of pairs) { + const url = `${raw}/${ref}/${canonical}`; + let localBytes; + try { + localBytes = Buffer.from(await readImpl(local)); + } catch (error) { + failures.push(`${local}: cannot read the local copy (${error.message})`); + continue; + } + + let response; + try { + response = await fetchImpl(url); + } catch (error) { + failures.push(`${local}: cannot fetch ${url} (${error.message})`); + continue; + } + if (response.status !== 200) { + failures.push(`${local}: ${url} answered ${response.status}, expected 200`); + continue; + } + const remoteBytes = Buffer.from(await response.arrayBuffer()); + if (remoteBytes.length === 0) { + failures.push(`${local}: ${url} returned an empty body`); + continue; + } + + checked += 1; + if (!localBytes.equals(remoteBytes)) { + failures.push( + `${local}: differs from ${canonical} at ${ref} ` + + `(${localBytes.length} bytes here, ${remoteBytes.length} canonical). ` + + `Copy the canonical file over it; change the canonical in ichava/js-ui-kernel first.`, + ); + } + } + return { checked, failures }; +} + +async function main() { + let pairs; + try { + pairs = parsePairs(process.argv.slice(2)); + } catch (error) { + console.error(`check-config-drift: ${error.message}`); + return 2; + } + const ref = process.env.ICHAVA_KERNEL_REF || 'main'; + const raw = process.env.ICHAVA_KERNEL_RAW || DEFAULT_RAW; + const { checked, failures } = await checkDrift({ pairs, ref, raw }); + + for (const failure of failures) console.error(`check-config-drift: ${failure}`); + if (failures.length > 0 || checked !== pairs.length) { + console.error(`check-config-drift: FAILED, ${checked} of ${pairs.length} file(s) compared`); + return 1; + } + console.log(`check-config-drift: ${checked} file(s) identical to ichava/js-ui-kernel@${ref}`); + return 0; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = await main(); +} diff --git a/.dev/browser/check-consumers.mjs b/.dev/browser/check-consumers.mjs new file mode 100644 index 0000000..c4b3e7a --- /dev/null +++ b/.dev/browser/check-consumers.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// Every place this package is used must carry its latest release before another one is cut. +// +// node scripts/check-consumers.mjs compare against the newest vX.Y.Z tag +// node scripts/check-consumers.mjs --version X compare against X instead +// node scripts/check-consumers.mjs --self-test prove the comparison can fail +// +// The consumers and the command that updates each are listed in scripts/consumers.json. A +// consumer's version is the `version` in its vendored vendored.json on its default branch, read +// through `gh api` (GH_TOKEN must be able to read the consumer). release-please.yml runs this +// before it opens a release pull request or tags one, so a release cannot stack on one that never +// reached icon-browser, ichava.com, the demo and the docs (the rule in CONTRIBUTING.md). +// +// A consumer's version only says which release it meant to vendor; its `commit` (vendored.json) must also be the +// release commit or a descendant of it, checked with `git merge-base --is-ancestor`, which needs the tag and history. +// +// Byte-identical in react-icon-browser and react-landingpage; only consumers.json differs. + +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; + +const SEMVER = /^v?(\d+)\.(\d+)\.(\d+)$/; + +/** -1, 0 or 1. Throws on anything that is not a plain X.Y.Z. */ +export function compareVersions(a, b) { + const pa = SEMVER.exec(a); + const pb = SEMVER.exec(b); + if (!pa || !pb) throw new Error(`not a release version: ${!pa ? a : b}`); + for (let i = 1; i <= 3; i++) { + const d = Number(pa[i]) - Number(pb[i]); + if (d !== 0) return d < 0 ? -1 : 1; + } + return 0; +} + +/** + * The consumers whose vendored copy is not `target` or newer, with what to tell the reader. + * + * The version alone is not enough: a copy built before the release commit can carry the release's + * version number, so a consumer one commit behind passes a version check. When `contains` is given + * it is asked whether the consumer's vendored commit is the release commit or a descendant of it + * (`true`), is not (`false`), or cannot be seen in this checkout (`null`). Anything but `true`, and + * a manifest that names no commit, is reported: a copy that cannot be shown current is not current. + * Without `contains` only the version is compared. + */ +export function behind(target, consumers, contains) { + const problems = []; + for (const c of consumers) { + const update = `\n Update it: ${c.update.replaceAll('{version}', target)}`; + if (c.version === null || compareVersions(c.version, target) < 0) { + problems.push(`${c.repo} vendors ${c.version ?? 'nothing readable'} at ${c.manifest}, not ${target}.${update}`); + } else if (contains && compareVersions(c.version, target) === 0) { + // A newer version is judged by the version; only an equal one can hide a stale commit. + const verdict = c.commit ? contains(c.commit) : null; + if (verdict !== true) { + const named = c.commit ? c.commit.slice(0, 12) : 'no commit'; + const why = verdict === false ? `which is not v${target} or a descendant of it` : 'which this checkout cannot confirm descends from the release (fetch tags and history, or the manifest names none)'; + problems.push(`${c.repo} vendors version ${c.version} at ${named}, ${why} (${c.manifest}).${update}`); + } + } + } + return problems; +} + +/** Whether `commit` is the commit `v` points at or descends from it; null when unknown here. */ +function descendsFromTag(target) { + return (commit) => { + try { + execFileSync('git', ['merge-base', '--is-ancestor', `v${target}`, commit], { stdio: 'ignore' }); + return true; + } catch (e) { + // Exit 1 is a definite "no"; any other status (unknown object, no tag) is "cannot tell". + return e.status === 1 ? false : null; + } + }; +} + +function latestTag() { + const tags = execFileSync('git', ['tag', '--list', 'v*'], { encoding: 'utf8' }) + .split('\n') + .map((t) => t.trim()) + .filter((t) => SEMVER.test(t)); + if (tags.length === 0) throw new Error('no vX.Y.Z tag in this checkout; fetch tags first (actions/checkout fetch-depth: 0)'); + return tags.sort(compareVersions).at(-1).replace(/^v/, ''); +} + +function vendoredManifest({ repo, manifest }) { + try { + const raw = execFileSync('gh', ['api', `repos/${repo}/contents/${manifest}`, '-H', 'Accept: application/vnd.github.raw'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + const parsed = JSON.parse(raw); + return { manifest, version: parsed.version ?? null, commit: typeof parsed.commit === 'string' ? parsed.commit : null }; + } catch (e) { + throw new Error(`could not read ${repo}:${manifest} (${String(e.stderr || e.message).trim()}). The token needs read access to ${repo}.`); + } +} + +function selfTest() { + const c = (version) => [{ repo: 'o/consumer', manifest: 'vendored.json', version, update: 'sync {version}' }]; + const cases = [ + [behind('0.1.3', c('0.1.2')).length === 1, 'an older consumer is reported'], + [behind('0.1.3', c('0.1.3')).length === 0, 'a current consumer passes'], + [behind('0.1.3', c('0.2.0')).length === 0, 'a newer consumer passes'], + [behind('0.10.0', c('0.9.9')).length === 1, 'versions compare numerically, not as strings'], + [behind('0.1.3', c(null)).length === 1, 'an unreadable version is reported'], + [(behind('0.1.3', c('0.1.2'))[0] ?? '').includes('sync 0.1.3'), 'the message names the target in the update command'], + [behind('0.1.3', [{ ...c('0.1.3')[0], commit: 'x' }], () => false).length === 1, 'the right version at a stale commit is reported'], + [behind('0.1.3', [{ ...c('0.1.3')[0], commit: 'x' }], () => true).length === 0, 'the right version at the release commit or later passes'], + [behind('0.1.3', [{ ...c('0.1.3')[0], commit: 'x' }], () => null).length === 1, 'a commit that cannot be confirmed is reported'], + [behind('0.1.3', c('0.1.3'), () => true).length === 1, 'a manifest naming no commit is reported when commits are checked'], + ]; + const failed = cases.filter(([ok]) => !ok).map(([, name]) => name); + if (failed.length) { + console.error(`check-consumers self-test failed:\n- ${failed.join('\n- ')}`); + process.exit(1); + } + console.log(`check-consumers self-test: ${cases.length} cases pass`); +} + +function main(argv) { + if (argv.includes('--self-test')) return selfTest(); + const at = argv.indexOf('--version'); + const target = at >= 0 ? argv[at + 1].replace(/^v/, '') : latestTag(); + const listed = JSON.parse(readFileSync(new URL('./consumers.json', import.meta.url), 'utf8')); + if (!Array.isArray(listed) || listed.length === 0) throw new Error('scripts/consumers.json lists no consumers'); + const consumers = listed.map((c) => ({ ...c, ...vendoredManifest(c) })); + for (const c of consumers) console.log(`${c.repo}: vendors ${c.version ?? '?'} at ${c.commit?.slice(0, 12) ?? '?'} (target ${target})`); + const problems = behind(target, consumers, descendsFromTag(target)); + if (problems.length) { + console.error(`\n${problems.length} consumer(s) behind ${target}. A release must reach every consumer before the next one:\n`); + for (const p of problems) console.error(`- ${p}`); + process.exit(1); + } + console.log(`all ${consumers.length} consumer(s) carry ${target} or newer`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + try { + main(process.argv.slice(2)); + } catch (e) { + console.error(`check-consumers: ${e.message}`); + process.exit(1); + } +} diff --git a/.dev/browser/check-dead-classes.mjs b/.dev/browser/check-dead-classes.mjs new file mode 100644 index 0000000..20df0ef --- /dev/null +++ b/.dev/browser/check-dead-classes.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Fails the build when a class name written in the source generates no CSS. + * + * Tailwind 4 drops a utility it cannot resolve without a warning, so the page renders + * with that property simply absent. Nothing else notices: the build is green, the + * typechecker has no opinion about strings, ESLint has no opinion about strings, and a + * screenshot test only catches it if that exact element is both rendered and visible in + * a baseline. `text-background` survived that way -- a leftover from an earlier component library + * `--color-background` token, removed when the app adopted Untitled UI's token layer, + * leaving a clear-filter glyph inheriting its colour instead of contrasting with the + * circle behind it. + * + * The check is deliberately mechanical: read the CSS the build actually emitted, not the + * source that was meant to produce it. Grepping the Tailwind config or the component + * proves how the class was written, never what the compiler kept. + * + * Run after `npm run build`, against `dist/`. + * + * WHAT IT DOES NOT COVER, so nobody reads a pass as more than it is: + * + * - Only literal double-quoted attributes (`className="..."`, `class="..."`, and the + * `*ClassName="..."` props). A class assembled in a template literal, a `clsx`/`cva` + * call, or a variable is invisible here. This is a floor, not a ceiling. + * - Every emitted bundle is treated as one pool. A class present in `main-*.css` but + * used on a page that only loads `landing-*.css` counts as alive, because attributing + * a component to an entry point needs the module graph. The bug class this exists to + * catch -- a class that generates nothing ANYWHERE -- is caught exactly. + */ + +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; +import { join, extname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const CSS_DIR = join(ROOT, 'dist/assets'); +const SRC_DIR = join(ROOT, 'src'); + +/** + * Class names that are expected to emit nothing, each with the reason it is not a defect. + * + * An entry that STARTS matching is itself a failure. Otherwise the list quietly turns + * into a place where real regressions go to be ignored: the day someone gives `ich-spin` + * real styles, the allowance would keep suppressing it forever. + */ +const STYLELESS = new Map([]); + +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']); + +/** + * Matches `class`, `className`, and prefixed pass-through props such as `inputClassName` + * or `popoverClassName`, but only where the value is a plain double-quoted string. + */ +const CLASS_ATTR = /(?:^|[^a-zA-Z])[a-zA-Z]*(?:[cC]lassNames?|class)\s*=\s*"([^"]*)"/g; + +function walk(dir, out = []) { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + if (statSync(path).isDirectory()) walk(path, out); + else if (SOURCE_EXTENSIONS.has(extname(path))) out.push(path); + } + return out; +} + +/** token -> Set of "file:line" */ +function collectTokens() { + const tokens = new Map(); + + const record = (token, file, line) => { + if (!tokens.has(token)) tokens.set(token, new Set()); + tokens.get(token).add(`${relative(ROOT, file)}:${line}`); + }; + + const scan = (file) => { + const source = readFileSync(file, 'utf8'); + CLASS_ATTR.lastIndex = 0; + let match; + while ((match = CLASS_ATTR.exec(source)) !== null) { + const line = source.slice(0, match.index).split('\n').length; + // A `${...}` interpolation is not a class name: it is how a snippet template writes + // a class the host configures (modules/snippets), so there is nothing to look up. + for (const token of match[1].split(/\s+/).filter(Boolean)) if (!token.includes('${')) record(token, file, line); + } + }; + + walk(SRC_DIR).forEach(scan); + readdirSync(ROOT) + .filter((f) => f.endsWith('.html')) + .forEach((f) => scan(join(ROOT, f))); + + return tokens; +} + +/** + * Tailwind escapes the characters that are not bare-identifier material, so `h-[17px]` + * ships as `.h-\[17px\]`. Dropping every backslash turns the selector back into the token + * as it was authored, which is what we have to compare against. + */ +function loadCss() { + if (!existsSync(CSS_DIR)) { + fail(`No ${relative(ROOT, CSS_DIR)} directory. Run \`npm run build\` first.`); + } + const files = readdirSync(CSS_DIR).filter((f) => f.endsWith('.css')); + if (files.length === 0) { + fail(`No CSS in ${relative(ROOT, CSS_DIR)}. Run \`npm run build\` first.`); + } + const css = files.map((f) => readFileSync(join(CSS_DIR, f), 'utf8')).join('\n'); + return { css: css.replace(/\\/g, ''), files }; +} + +/** + * A selector ends at one of these. Requiring one stops `.w-full` from being satisfied by + * `.max-w-full`, and lets a token still count when the rule continues -- `.text-secondary` + * inside `.text-secondary:hover`, or the attribute suffix Tailwind appends to a `data-*` + * variant, as in `.data-focused:bg-secondary[data-focused]`. + */ +const SELECTOR_BOUNDARY = new Set(['{', ':', ',', ' ', '>', '+', '~', '[', '.', ')']); + +function emitsCss(token, css) { + const needle = `.${token}`; + let at = css.indexOf(needle); + while (at !== -1) { + const next = css[at + needle.length]; + if (next !== undefined && SELECTOR_BOUNDARY.has(next)) return true; + at = css.indexOf(needle, at + 1); + } + return false; +} + +function fail(message) { + console.error(`\ncheck-dead-classes: ${message}\n`); + process.exit(1); +} + +const { css, files } = loadCss(); +const tokens = collectTokens(); + +if (tokens.size === 0) { + fail('Found no class attributes to check at all, which means the scan is broken.'); +} + +const dead = []; +const staleAllowances = []; + +for (const [token, locations] of [...tokens].sort(([a], [b]) => a.localeCompare(b))) { + const present = emitsCss(token, css); + if (STYLELESS.has(token)) { + if (present) staleAllowances.push(token); + } else if (!present) { + dead.push([token, [...locations].sort()]); + } +} + +console.log(`Checked ${tokens.size} class names against ${files.length} CSS bundle(s): ${files.join(', ')}`); + +if (staleAllowances.length > 0) { + console.error('\nThese are listed as styleless but now emit CSS. Remove them from STYLELESS:'); + for (const token of staleAllowances) console.error(` ${token}`); +} + +if (dead.length > 0) { + console.error('\nThese class names generate no CSS. Tailwind dropped them silently:'); + for (const [token, locations] of dead) { + console.error(`\n ${token}`); + for (const location of locations) console.error(` ${location}`); + } + console.error( + '\nEither the class is misspelled, or it names a token that no longer exists.\n' + + 'If it is intentionally styleless, add it to STYLELESS with the reason.', + ); +} + +if (dead.length > 0 || staleAllowances.length > 0) process.exit(1); + +console.log(`No dead class names. ${STYLELESS.size} known styleless class(es) skipped.`); diff --git a/.dev/browser/check-dead-links.mjs b/.dev/browser/check-dead-links.mjs new file mode 100644 index 0000000..5b4f717 --- /dev/null +++ b/.dev/browser/check-dead-links.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +// Fails on a link to a documentation-portal path that does not exist (A6-gaps G14). +// +// The documentation portal's /documentation/ichava/... and /products/ichava/... paths +// answer 404: it has no Ichava pages yet. Until it does, links point at the +// repository's GitHub docs/ folder. This walks every tracked text file and fails on the +// dead prefix, on either host name (oss.simtabi.com is an alias). +// +// CHANGELOG.md is exempt: it records what was true when each entry was written. +// +// It also resolves every relative link in a tracked Markdown file (`[text](docs/x.md#y)`) against +// that file's directory and fails when the target is not on disk: docs/architecture.md once +// linked docs/host-integration.md from inside docs/, which resolves to docs/docs/. +// +// The file walk asserts a floor on what it read, and the pattern is first run against a +// planted sample, so a walk that found nothing or a pattern that stopped matching fails +// here instead of passing everything. + +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +const DEAD = /https?:\/\/(?:opensource|oss)\.simtabi\.com\/(?:documentation|products)\/ichava\b/; +const EXEMPT = new Set(['CHANGELOG.md']); +const TEXT = /\.(?:[cm]?[jt]sx?|json|md|html|css|ya?ml|txt)$/; +const FLOOR = 20; + +// Assembled from parts, so this file does not contain the prefix it forbids. +const planted = [ + 'href="https://opensource.simtabi.com/' + 'documentation/ichava/icon-browser/"', + 'https://oss.simtabi.com/' + 'products/ichava/icon-browser', +]; +const missed = planted.filter((s) => !DEAD.test(s)); +if (DEAD.test('https://github.com/ichava/icon-browser/tree/main/docs')) missed.push('(matched a live link)'); +if (missed.length) { + console.error(`check-dead-links: self-test failed on ${missed.join(', ')}`); + process.exit(1); +} + +const files = execFileSync('git', ['ls-files', '-z'], { encoding: 'utf8' }) + .split('\0') + .filter((f) => f && TEXT.test(f) && !EXEMPT.has(f)); +if (files.length < FLOOR) { + console.error(`check-dead-links: read ${files.length} files, expected at least ${FLOOR}`); + process.exit(1); +} + +// A relative Markdown link target: not a URL, not an in-page anchor, not an absolute path. +const RELATIVE_LINK = /\]\((?![a-z][a-z0-9+.-]*:|#|\/)([^)\s#]+)(?:#[^)\s]*)?\)/gi; +if ([...'[a](docs/x.md#y) [b](https://e.test) [c](#z)'.matchAll(RELATIVE_LINK)].map((m) => m[1]).join() !== 'docs/x.md') { + console.error('check-dead-links: self-test failed on the relative-link pattern'); + process.exit(1); +} + +const hits = []; +let relative = 0; +for (const file of files) { + const lines = readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + if (DEAD.test(line)) hits.push(`${file}:${i + 1}: ${line.trim().slice(0, 160)}`); + if (!file.endsWith('.md')) return; + for (const m of line.matchAll(RELATIVE_LINK)) { + relative++; + const target = join(dirname(file), decodeURIComponent(m[1])); + if (!existsSync(target)) hits.push(`${file}:${i + 1}: relative link ${m[1]} resolves to ${target}, which does not exist`); + } + }); +} +if (hits.length) { + for (const h of hits) console.error(`check-dead-links: ${h}`); + console.error( + 'check-dead-links: FAILED. The portal has no Ichava pages yet; link the GitHub docs/ folder ' + + '(https://github.com/ichava//tree/main/docs) instead, and point each relative link at a file that exists.', + ); + process.exit(1); +} +console.log(`check-dead-links: ok -- ${files.length} tracked files, no dead portal links, ${relative} relative Markdown links resolve`); diff --git a/.dev/browser/check-lib-build.mjs b/.dev/browser/check-lib-build.mjs new file mode 100644 index 0000000..8b40812 --- /dev/null +++ b/.dev/browser/check-lib-build.mjs @@ -0,0 +1,249 @@ +#!/usr/bin/env node +// Asserts dist/lib is something a host can consume (FINDINGS B1, B2). Run after +// `npm run build:lib`; CI runs it on every pull request. +// +// 1. no `@/` alias specifier survives in the JS or the declarations +// 2. the JS imports no stylesheet: styles are the host's explicit opt-in +// 3. the peers stay bare imports, so the host's single React is the one used +// 4. styles.css sets no overflow on html, body or #root +// 5. every url() in styles.css is relative and names a file that exists in dist/lib, +// so the fonts load in a host that serves nothing at /fonts/ +// 6. styles.css declares no custom property on :root, :host, html, body or a bare +// .dark-mode (FINDINGS N1): those match the HOST's document, so a second package's +// tokens and ours overwrite each other in load order. Tokens live on +// [data-ichava-theme], which the component root and its overlay container carry. +// 7. every utility sits inside `@scope ([data-ichava-theme])`, so a host's own Tailwind +// cannot override it by loading later (see scripts/utility-scope.mjs) +// 8. index.js is fully minified and names no source map: it is vendored into public +// repositories, and Untitled UI PRO's licence bars exposing its source +// +// Each predicate is first run against a planted defect and must report it, so a +// regex that stopped matching fails here instead of passing everything. + +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { cssRules, stripCssComments } from '@ichava/js-ui-kernel/build'; +import { globalTokenRules } from '@ichava/js-ui-kernel/build'; +import { reachingRules, scopeSheet, scopeUtilities, UTILITY_SCOPE } from '../../vite.utility-scope.mjs'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const lib = join(root, 'dist/lib'); +const problems = []; + +const ALIAS = /(?:from\s*|import\s*\(\s*|import\s+)(['"])@\/[^'"]*\1/; +const CSS_IMPORT = /(?:from\s*|import\s*\(\s*|import\s+)(['"])[^'"]+\.css\1/; + +/** Rules whose selector list names html, body or #root and whose body sets overflow. */ +function documentLocks(css) { + const out = []; + // The kernel's one-pass scanner: the comment and rule regexes it replaces retried from + // every offset and took seconds on hostile input (CodeQL js/polynomial-redos). + for (const { selector, body } of cssRules(stripCssComments(css))) { + const names = selector.split(',').map((s) => s.trim()); + const hit = names.some((s) => /^(html|body|#root)\b/.test(s) || /[\s>+~](html|body|#root)\b/.test(s)); + if (hit && /(^|;)\s*overflow(-[xy])?\s*:/.test(body)) out.push(selector.trim()); + } + return out; +} + +/** + * Peers and the react-aria scopes, which the host provides. react-aria-components + * re-exports from @react-aria/*, @react-stately/* and @react-types/*; a bundled copy of + * any of them splits focus management from the host's (behaviour change 5). + */ +const PEERS = ['react', 'react-dom', 'react-aria', 'react-aria-components', 'react-stately', 'zustand', '@react-aria', '@react-stately', '@react-types']; + +/** Bundled sources, from the sourcemap, that belong to a peer. */ +function bundledPeers(sources) { + return sources.filter((src) => { + const m = src.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/); + return m && PEERS.some((p) => m[1] === p || m[1].startsWith(`${p}/`)); + }); +} + +/** url() targets in a stylesheet that do not resolve to a file next to it. */ +function unresolvedUrls(css, cssDir, exists) { + const out = []; + for (const [, , target] of css.matchAll(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g)) { + if (target.startsWith('data:') || target.startsWith('#')) continue; + if (/^([a-z]+:)?\//i.test(target)) out.push(`${target} (absolute: resolves against the host, not the package)`); + else if (!exists(join(cssDir, target.split(/[?#]/)[0]))) out.push(`${target} (no such file in dist/lib)`); + } + return out; +} + +/** + * Why `code` is not a fully minified bundle: a source map named, readable layout (short + * lines or indentation), a comment that is not a licence or pure annotation, or long + * declared names. Template literals keep their own newlines, so the layout limits are + * per byte rather than zero. icon-browser's `minifiedBundle.ts` applies the same rules + * to the vendored copy. + */ +function notMinified(code) { + const out = []; + const lines = code.split('\n'); + if (/sourceMappingURL/.test(code)) out.push('names a source map (sourceMappingURL)'); + if (code.length / lines.length < 1000) out.push(`averages ${Math.round(code.length / lines.length)} bytes a line (minified: 1000+)`); + const indented = lines.filter((l) => /^[ \t]{2,}\S/.test(l)).length; + if (indented * 10_000 > code.length) out.push(`${indented} indented lines (at most one per 10 kB)`); + const comments = [...code.matchAll(/(? m[1]); + const long = names.filter((n) => n.length > 3).length; + if (names.length && long / names.length > 0.05) out.push(`${long} of ${names.length} declared names are longer than 3 characters (unmangled)`); + return out; +} + +// Self-checks: each guard must catch a planted defect. +if (bundledPeers(['../../node_modules/react/cjs/react.js', '../../node_modules/@react-stately/utils/dist/x.mjs']).length !== 2) + problems.push('self-check: bundled peer sources were not reported'); +if (bundledPeers(['../../node_modules/tailwind-merge/dist/x.mjs', '../../src/index.ts', '../../node_modules/react-x/y.js']).length !== 0) + problems.push('self-check: non-peer sources reported as peers'); +// The scan is linear: hostile input finishes in well under 50 ms. +for (const hostile of [`/${'a/'.repeat(100_000)}`, 'z'.repeat(100_000)]) { + const start = performance.now(); + documentLocks(hostile); + const ms = performance.now() - start; + if (ms >= 50) problems.push(`self-check: scanning ${hostile.length} hostile characters took ${ms.toFixed(0)} ms (limit 50)`); +} +{ + const readable = `// a comment\nfunction resolveConfig(input) {\n const merged = { ...input };\n return merged;\n}\n//# sourceMappingURL=index.js.map\n`.repeat(50); + if (notMinified(readable).length < 4) problems.push(`self-check: a readable bundle passed the minification check (${notMinified(readable).join('; ')})`); + const minified = `/*! licence */import{a as b}from"react";function c(d){return/* @__PURE__ */b(d)}`.padEnd(20_000, ';') + '`\n x`'; + if (notMinified(minified).length) problems.push(`self-check: a minified bundle failed the minification check (${notMinified(minified).join('; ')})`); +} +if (!ALIAS.test(`export { X } from '@/IchavaBrowser';`)) problems.push('self-check: alias pattern misses a planted `@/` import'); +if (ALIAS.test(`export { X } from './IchavaBrowser';`)) problems.push('self-check: alias pattern flags a relative import'); +if (!CSS_IMPORT.test(`import "./styles.css";`)) problems.push('self-check: css-import pattern misses a planted import'); +if (documentLocks('@layer base{html,body,#root{margin:0;height:100%;overflow:hidden}}').length !== 1) + problems.push('self-check: overflow lock not detected in a planted rule'); +if (documentLocks('html,:host{line-height:1.5}body{margin:0}[data-ichava-root]{overflow:hidden}').length !== 0) + problems.push('self-check: overflow lock reported where there is none'); + +if (unresolvedUrls("@font-face{src:url('/fonts/G.woff2')}", '/nowhere', () => true).length !== 1) + problems.push('self-check: an absolute font url() was not reported'); +if (unresolvedUrls('@font-face{src:url(./fonts/G.woff2)}', '/nowhere', () => false).length !== 1) + problems.push('self-check: a relative url() to a missing file was not reported'); +if (unresolvedUrls('a{b:url("data:image/svg+xml,x")}', '/nowhere', () => false).length !== 0) + problems.push('self-check: a data: url() was reported'); + +if (globalTokenRules('@layer theme{:root,:host{--color-brand-500:#7f56d9}}').length !== 1) + problems.push('self-check: a token on :root,:host was not reported'); +if (globalTokenRules('@layer base{.dark-mode{--bg:#000}}html{--x:1}body{--y:2}').length !== 3) + problems.push('self-check: a token on .dark-mode, html or body was not reported'); +if (globalTokenRules('[data-ichava-theme]{--bg:#fff}[data-ichava-theme].dark-mode{--bg:#000}:root{color:red}@property --tw-x{syntax:"*"}').length !== 0) + problems.push('self-check: a scoped token rule, a plain :root rule or an @property was reported'); + +/** `@layer utilities` blocks whose body does not open with the utility scope. */ +function unscopedUtilityLayers(css) { + const open = '@layer utilities{'; + const scoped = `${open}@scope (${UTILITY_SCOPE}){`; + let n = 0; + for (let i = css.indexOf(open); i !== -1; i = css.indexOf(open, i + 1)) if (!css.startsWith(scoped, i)) n += 1; + return n; +} +if (unscopedUtilityLayers('@layer utilities{.px-3{padding-inline:12px}}') !== 1) + problems.push('self-check: an unscoped utilities layer was not reported'); +if (unscopedUtilityLayers(scopeUtilities('@layer base{a{b:c}}@layer utilities{.a{b:"}"}@media (x){.c{d:e}}}.z{y:x}')) !== 0) + problems.push('self-check: scopeUtilities left a utilities layer unscoped'); +if (scopeUtilities('@layer utilities{.a{b:c}}.z{y:x}') !== `@layer utilities{@scope (${UTILITY_SCOPE}){.a{b:c}}}.z{y:x}`) + problems.push('self-check: scopeUtilities did not close the scope where the layer closes'); +// The element-rule guard can fail (an unscoped `button:focus` and preflight layer are +// reported) and can pass (the same sheet after scopeSheet reports nothing; own rules stay). +{ + const plant = '@layer base{a{b:c}}button:focus{outline:none}@media (x){input{a:b}.ichbs{c:d}}.ich-x{a:b}[data-ichava-theme] i{a:b}'; + const before = reachingRules(plant); + if (before.length !== 3) problems.push(`self-check: reachingRules reported ${before.length} of 3 planted host-reaching rules`); + const scoped = scopeSheet(plant); + if (reachingRules(scoped).length !== 0) problems.push('self-check: scopeSheet left a host-reaching rule'); + // A rule naming the root must stay outside @scope, or it stops matching the root itself. + if (scopeSheet('@layer base{[data-ichava-theme]{--m:1}}') !== '@layer base{[data-ichava-theme]{--m:1}}') + problems.push('self-check: scopeSheet wrapped a rule that names the root'); + if (!scopeSheet('@layer base{*{a:b}}').includes(':scope,*{a:b}')) problems.push('self-check: a scoped universal rule no longer reaches the root'); +} + +function walk(dir, ext) { + return readdirSync(dir).flatMap((n) => { + const p = join(dir, n); + return statSync(p).isDirectory() ? walk(p, ext) : p.endsWith(ext) ? [p] : []; + }); +} + +/** + * The root's typeface. Since #173 Tailwind's preflight also matches the browser root, and + * it sets the font from `--default-font-family`, which is `--font-sans`. Unless the sheet + * points `--font-sans` at the body font (and that at Geist), every built page renders in + * the system stack and Geist never loads -- what the 2026-10-28 baselines showed. + */ +function rootFontIsGeist(sheet) { + return /--font-sans:\s*var\(--font-body\)/.test(sheet) && /--font-inter:\s*["']?Geist\b/.test(sheet); +} +if (!rootFontIsGeist('[data-ichava-theme]{--font-inter:"Geist";--font-sans:var(--font-body)}')) problems.push('self-check: a Geist root font was not recognised'); +if (rootFontIsGeist('[data-ichava-theme]{--font-inter:"Geist";--font-sans:-apple-system,sans-serif}')) problems.push('self-check: a system-stack --font-sans was accepted'); + +const js = join(lib, 'index.js'); +const css = join(lib, 'styles.css'); +const types = join(lib, 'types/index.d.ts'); +for (const f of [js, css, types]) if (!existsSync(f)) problems.push(`missing ${relative(root, f)} -- run npm run build:lib`); + +if (problems.length === 0) { + const jsFiles = walk(lib, '.js'); + const dts = walk(join(lib, 'types'), '.d.ts'); + // Floors, so a build that emitted almost nothing cannot pass by having nothing to check. + if (dts.length < 50) problems.push(`only ${dts.length} declaration files under dist/lib/types (expected 50+)`); + + for (const f of [...jsFiles, ...dts]) { + const lines = readFileSync(f, 'utf8').split('\n'); + lines.forEach((line, i) => { + if (ALIAS.test(line)) problems.push(`${relative(root, f)}:${i + 1} keeps an @/ alias: ${line.trim().slice(0, 120)}`); + }); + } + + // index.js and the chunks the on-demand layers load (re-audit F-12): every one is minified, + // imports no stylesheet and bundles no peer; the peers are imported as externals across them. + const chunks = jsFiles.filter((f) => !f.includes(`${join(lib, 'types')}`)); + let all = ''; + for (const f of chunks) { + const name = relative(root, f); + const code = readFileSync(f, 'utf8'); + all += code; + if (CSS_IMPORT.test(code)) problems.push(`${name} imports a stylesheet; the host must opt in instead`); + const map = `${f}.map`; + if (!existsSync(map)) problems.push(`${name}.map is missing; the bundled-peer check needs it`); + else for (const src of bundledPeers(JSON.parse(readFileSync(map, 'utf8')).sources)) problems.push(`${name} bundles peer code: ${src}`); + } + // Measured over all of them together: each small chunk repeats the licence banner's lines. + for (const why of notMinified(all)) problems.push(`dist/lib/*.js is not fully minified: ${why}`); + for (const peer of ['react', 'react-dom/client', 'react-aria-components', 'zustand']) { + if (!new RegExp(`from\\s*["']${peer}["']`).test(all)) problems.push(`dist/lib does not import "${peer}" as an external`); + } + + const sheet = readFileSync(css, 'utf8'); + if (sheet.length < 20_000) problems.push(`dist/lib/styles.css is ${sheet.length} bytes; expected the full prebuilt stylesheet`); + if (!sheet.includes('[data-ichava-root]')) problems.push('dist/lib/styles.css lacks the component-root rules; is it the right file?'); + for (const sel of documentLocks(sheet)) problems.push(`dist/lib/styles.css locks the host document: ${sel}`); + for (const sel of globalTokenRules(sheet)) + problems.push(`dist/lib/styles.css declares custom properties on the host document: ${sel} {...} (scope them to [data-ichava-theme])`); + if (!sheet.includes('@layer utilities{')) problems.push('dist/lib/styles.css has no utilities layer; is it the right file?'); + const reaching = reachingRules(sheet); + if (reaching.length) problems.push(`dist/lib/styles.css has ${reaching.length} rule(s) that reach a host's own elements; scope them (scripts/utility-scope.mjs): ${reaching.slice(0, 6).join(' | ')}`); + if (unscopedUtilityLayers(sheet) > 0) problems.push(`dist/lib/styles.css has ${unscopedUtilityLayers(sheet)} utilities layer(s) outside @scope (${UTILITY_SCOPE}); a host's Tailwind would override them`); + if (!sheet.includes('[data-ichava-theme]')) problems.push('dist/lib/styles.css declares no [data-ichava-theme] tokens; the component would render unthemed'); + + if (!rootFontIsGeist(sheet)) problems.push('dist/lib/styles.css does not route --font-sans to Geist; the scoped preflight would set the root in the system font'); + + const fontUrls = [...sheet.matchAll(/url\(\s*['"]?[^'")]*\.woff2/g)].length; + if (fontUrls < 2) problems.push(`dist/lib/styles.css references ${fontUrls} woff2 file(s); expected Geist and Geist Mono`); + for (const bad of unresolvedUrls(sheet, lib, existsSync)) problems.push(`dist/lib/styles.css url() does not resolve: ${bad}`); + if (!existsSync(join(lib, 'fonts/LICENSE.txt'))) problems.push('dist/lib/fonts/LICENSE.txt is missing; the fonts ship under the OFL'); + + if (problems.length === 0) { + console.log(`check-lib-build: ok -- ${jsFiles.length} js, ${dts.length} d.ts, styles.css ${sheet.length} bytes, ${fontUrls} font url()s resolved, no @/ specifiers, no document lock, no document-level tokens, utilities and element rules scoped`); + } +} + +if (problems.length) { + for (const p of problems) console.error(`::error::${p}`); + process.exit(1); +} diff --git a/.dev/browser/check-playwright-image.mjs b/.dev/browser/check-playwright-image.mjs new file mode 100644 index 0000000..8bb027d --- /dev/null +++ b/.dev/browser/check-playwright-image.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/** + * Fails when the Playwright image tag drifts out of step with itself. + * + * The visual gate's correctness rests on one thing: that the image CI compares in, the + * image the seed workflow generates in, and the image `npm run visual:linux` runs are + * the SAME image. Font stacks differ between images, so a mismatch shifts text + * antialiasing by ~1% of pixels -- and the failure surfaces as a screenshot diff, which + * reads as a rendering regression rather than a configuration one. Somebody then spends + * an afternoon looking at the wrong thing. + * + * It also checks the tag against `@playwright/test`, because the image and the library + * ship the browsers together; a `v1.62.0-noble` image with a 1.63 dependency is a + * version skew that may or may not announce itself. + * + * This is a guard rather than a comment on purpose. The same trap was already described + * in prose -- in `public/fonts/README.md`, in the CHANGELOG, and in a CI error message -- + * and two separate sessions walked into it anyway. Prose does not fail a build. + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const IMAGE = /mcr\.microsoft\.com\/playwright:(v[\d.]+-\w+)/g; + +const sources = [ + ['.github/workflows/ci.yml', 'CI visual job container'], + ['.github/workflows/visual-baselines.yml', 'seed workflow container'], + ['scripts/linux.sh', 'npm run visual:linux, visual:linux:check and e2e:linux'], +]; + +const found = []; +for (const [file, label] of sources) { + const text = readFileSync(join(root, file), 'utf8'); + const tags = [...text.matchAll(IMAGE)].map((m) => m[1]); + if (tags.length === 0) { + console.error(`FAIL ${label} (${file}) names no Playwright image.`); + console.error(' All three must pin the same one, or the baselines they produce'); + console.error(' and the baselines CI compares will not match.'); + process.exit(1); + } + for (const t of tags) found.push({ file, label, tag: t }); +} + +const distinct = [...new Set(found.map((f) => f.tag))]; +if (distinct.length > 1) { + console.error(`FAIL ${distinct.length} different Playwright images are pinned:`); + for (const f of found) console.error(` ${f.tag} <- ${f.label} (${f.file})`); + console.error(''); + console.error(' They must be identical. A different image ships a different font'); + console.error(' stack, which moves text by ~1% of pixels -- enough to fail the visual'); + console.error(' gate, and it will look like a rendering regression, not a config one.'); + process.exit(1); +} + +const imageTag = distinct[0]; +const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); +const dep = pkg.devDependencies?.['@playwright/test'] ?? pkg.dependencies?.['@playwright/test']; +const depVersion = dep?.replace(/^[\^~>=<\s]*/, ''); +const imageVersion = imageTag.replace(/^v/, '').replace(/-\w+$/, ''); + +if (depVersion && depVersion !== imageVersion) { + console.error(`FAIL image is ${imageTag} but @playwright/test is ${dep}.`); + console.error(' The image ships the browsers that version of the library drives.'); + console.error(` Bump them together: image -> v${depVersion}-noble.`); + process.exit(1); +} + +console.log(`ok Playwright image ${imageTag} pinned consistently in ${found.length} places,`); +console.log(` matching @playwright/test ${dep}.`); diff --git a/.dev/browser/check-public-api.mjs b/.dev/browser/check-public-api.mjs new file mode 100644 index 0000000..2816824 --- /dev/null +++ b/.dev/browser/check-public-api.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node +// The public API may grow, never shrink or change (architecture.md 4.1). Run after +// `npm run build:lib`; CI runs it on every pull request. +// +// Reads the emitted dist/lib/types/index.d.ts with the TypeScript compiler API, lists +// every export the package entry exposes, and prints each one's declaration with +// comments and private members removed. That listing is compared with the committed +// snapshot, scripts/public-api.snapshot.json: +// +// - an entry in the snapshot that is missing now fails (a removal) +// - an entry whose declaration text differs fails (a change) +// - an entry that is new passes, and is listed +// - an entry that only gained members (same head line, +// every old line still there, in order) passes, and is listed +// - fewer exports read than the snapshot holds, or none fails (an empty read) +// +// After an intended addition, record it with `npm run check:public-api -- --update`. +// A removal or change is a decision for the owner, not something to update away. +// +// The comparison is first run against planted defects and must report each one, so a +// comparison that stopped seeing differences fails here instead of passing everything. + +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const entry = join(root, 'dist/lib/types/index.d.ts'); +const snapshotPath = join(root, 'scripts/public-api.snapshot.json'); + +/** Removes private members (TS `private` and `#private`) from a class declaration. */ +function publicOnly(node) { + if (!ts.isClassDeclaration(node)) return node; + const members = node.members.filter((m) => { + if (m.name && ts.isPrivateIdentifier(m.name)) return false; + const mods = ts.canHaveModifiers(m) ? ts.getModifiers(m) ?? [] : []; + return !mods.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword); + }); + return ts.factory.updateClassDeclaration( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + members, + ); +} + +/** + * A declaration re-exported under another name (`export { CatalogIndex as IconRepository }`) + * prints under its own name. What a consumer imports is the export name, so the head of + * the declaration is written with that name: renaming a class behind an unchanged export + * is not an API change, and an export that starts pointing at a different declaration + * still shows up as one. + */ +function asExported(text, decl, exportName) { + const own = decl.name && ts.isIdentifier(decl.name) ? decl.name.text : null; + if (!own || own === exportName) return text; + return text.replace(new RegExp(`\\b(class|interface|function|type|enum|const|let|var) ${own}\\b`), `$1 ${exportName}`); +} + +/** Every export of the entry: { name: declaration text }. */ +export function readApi(file) { + const program = ts.createProgram([file], { + noEmit: true, + skipLibCheck: true, + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + jsx: ts.JsxEmit.ReactJSX, + }); + const checker = program.getTypeChecker(); + const source = program.getSourceFile(file); + if (!source) throw new Error(`cannot read ${file}`); + const moduleSymbol = checker.getSymbolAtLocation(source); + if (!moduleSymbol) throw new Error(`${file} is not a module`); + + const printer = ts.createPrinter({ removeComments: true, newLine: ts.NewLineKind.LineFeed }); + const api = {}; + for (const exported of checker.getExportsOfModule(moduleSymbol)) { + const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported; + const texts = (symbol.declarations ?? []).map((decl) => + asExported(printer.printNode(ts.EmitHint.Unspecified, publicOnly(decl), decl.getSourceFile()).trim(), decl, exported.name), + ); + api[exported.name] = texts.join('\n'); + } + return api; +} + +/** + * Whether `now` only adds to `was`: the same first line (the declaration head: name, + * type parameters, heritage), and every line of `was` still present, in order. A class or + * interface that gains a member grows; one that loses or changes a member does not. + */ +function grew(was, now) { + const a = was.split('\n'); + const b = now.split('\n'); + if (a[0] !== b[0]) return false; + let j = 0; + for (const line of b) if (j < a.length && line === a[j]) j += 1; + return j === a.length; +} + +/** Compares a current listing with the snapshot. */ +export function compare(snapshot, current) { + const problems = []; + const added = []; + const grown = []; + const want = Object.keys(snapshot); + const have = Object.keys(current); + if (have.length === 0) problems.push('read 0 exports: the entry was empty or unreadable'); + if (have.length < want.length) { + problems.push(`read ${have.length} exports, the snapshot holds ${want.length}`); + } + for (const name of want) { + if (!(name in current)) problems.push(`removed: ${name}`); + else if (current[name] !== snapshot[name]) { + if (grew(snapshot[name], current[name])) grown.push(name); + else problems.push(`changed: ${name}\n was: ${snapshot[name]}\n now: ${current[name]}`); + } + } + for (const name of have) if (!(name in snapshot)) added.push(name); + return { problems, added, grown }; +} + +function selfTest() { + const base = { a: 'declare const a: number;', b: 'declare function b(): void;' }; + const cases = [ + ['a removal', { a: base.a }], + ['a change', { ...base, b: 'declare function b(x: string): void;' }], + ['an empty read', {}], + ]; + const missed = cases.filter(([, current]) => compare(base, current).problems.length === 0).map(([n]) => n); + if (compare(base, { ...base, c: 'declare const c: 1;' }).problems.length !== 0) missed.push('an allowed addition'); + const cls = 'declare class K {\n a(): void;\n b(): void;\n}'; + if (compare({ k: cls }, { k: 'declare class K {\n a(): void;\n c(): void;\n b(): void;\n}' }).problems.length !== 0) { + missed.push('an allowed new member'); + } + if (compare({ k: cls }, { k: 'declare class K {\n a(): void;\n}' }).problems.length === 0) missed.push('a removed member'); + if (compare({ k: cls }, { k: 'declare class K {\n a(): void;\n b(): number;\n}' }).problems.length === 0) missed.push('a changed member'); + if (compare({ k: cls }, { k: 'declare class K extends J {\n a(): void;\n b(): void;\n}' }).problems.length === 0) missed.push('a changed head'); + if (missed.length) { + console.error(`check-public-api: self-test failed, the comparison does not report ${missed.join(', ')}`); + process.exit(1); + } +} + +function main() { + selfTest(); + if (!existsSync(entry)) { + console.error(`check-public-api: ${entry} is missing. Run \`npm run build:lib\` first.`); + process.exit(1); + } + const current = readApi(entry); + if (process.argv.includes('--update')) { + writeFileSync(snapshotPath, JSON.stringify(current, null, 2) + '\n'); + console.log(`check-public-api: wrote ${Object.keys(current).length} exports to the snapshot`); + return; + } + if (!existsSync(snapshotPath)) { + console.error('check-public-api: no snapshot committed. Run with --update once, from main.'); + process.exit(1); + } + const snapshot = JSON.parse(readFileSync(snapshotPath, 'utf8')); + const { problems, added, grown } = compare(snapshot, current); + for (const name of added) console.log(`check-public-api: new export ${name} (record it with --update)`); + for (const name of grown) console.log(`check-public-api: ${name} gained members (record it with --update)`); + if (problems.length) { + for (const p of problems) console.error(`check-public-api: ${p}`); + console.error('check-public-api: FAILED. The public API may grow, never shrink or change.'); + process.exit(1); + } + console.log( + `check-public-api: ok -- ${Object.keys(current).length} exports read, ` + + `${Object.keys(snapshot).length - grown.length} in the snapshot unchanged, ${grown.length} grown, ${added.length} new`, + ); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/.dev/browser/checkConsumers.test.mts b/.dev/browser/checkConsumers.test.mts new file mode 100644 index 0000000..50b760c --- /dev/null +++ b/.dev/browser/checkConsumers.test.mts @@ -0,0 +1,57 @@ +// `node --test scripts/checkConsumers.test.mts`: W-js3. A consumer whose vendored.json names the +// right version but a commit that is not the release (or a descendant of it) must be reported. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +// @ts-expect-error plain .mjs without types +import { behind } from './check-consumers.mjs'; +import { siblingPath } from './siblings.mjs'; + +const TAG_COMMIT = 'a'.repeat(40); +const OLD = 'b'.repeat(40); +const NEWER = 'c'.repeat(40); +const consumer = (version: string | null, commit: string | null) => [ + { repo: 'o/consumer', manifest: 'vendored.json', version, commit, update: 'sync {version}' }, +]; +// The release commit and NEWER descend from it; OLD does not; anything else is unknown locally. +const contains = (commit: string): boolean | null => (commit === TAG_COMMIT || commit === NEWER ? true : commit === OLD ? false : null); + +test('the right version at an old commit is reported', () => { + const out = behind('0.1.0', consumer('0.1.0', OLD), contains); + assert.equal(out.length, 1); + assert.match(out[0], new RegExp(OLD.slice(0, 12))); +}); + +test('the release commit and a descendant of it pass', () => { + assert.equal(behind('0.1.0', consumer('0.1.0', TAG_COMMIT), contains).length, 0); + assert.equal(behind('0.1.0', consumer('0.1.0', NEWER), contains).length, 0); +}); + +test('a commit this checkout cannot see, or none at all, is reported rather than trusted', () => { + assert.equal(behind('0.1.0', consumer('0.1.0', 'd'.repeat(40)), contains).length, 1); + assert.equal(behind('0.1.0', consumer('0.1.0', null), contains).length, 1); +}); + +test('an older version is reported once, whatever the commit', () => { + assert.equal(behind('0.1.1', consumer('0.1.0', TAG_COMMIT), contains).length, 1); +}); + +test('without a commit check the version rule alone applies (callers that pass none)', () => { + assert.equal(behind('0.1.0', consumer('0.1.0', OLD)).length, 0); +}); + +const consumers = JSON.parse(readFileSync(new URL('./consumers.json', import.meta.url), 'utf8')) as { repo: string; manifest: string }[]; + +test('every consumer names its manifest as vendored.json, the one name the syncs write', () => { + assert.ok(consumers.length > 0, 'consumers.json lists no consumer'); + for (const c of consumers) assert.match(c.manifest, /(^|\/)vendored\.json$/, `${c.repo}: ${c.manifest}`); +}); + +for (const c of consumers) { + // The manifest is read from GitHub at release time. When the consumer is checked out beside this + // repository, its path is checked here too, so a move in the consumer fails before a release does. + const local = siblingPath(c.repo, c.manifest); + test(`${c.repo} keeps ${c.manifest} where consumers.json says`, { skip: local === null ? `${c.repo} is not checked out beside this repo` : false }, () => { + assert.ok(existsSync(local!), `${local} does not exist`); + }); +} diff --git a/.dev/browser/consumers.json b/.dev/browser/consumers.json new file mode 100644 index 0000000..14c9a7b --- /dev/null +++ b/.dev/browser/consumers.json @@ -0,0 +1,7 @@ +[ + { + "repo": "ichava/icon-browser", + "manifest": "resources/vendor/react-icon-browser/vendored.json", + "update": "run `npm run build:lib` in react-icon-browser at v{version}, then in ichava/icon-browser run `node .dev/tools/sync-react-icon-browser.mjs ` and `npm run build:prod`; then `composer update` in the local demos/ichava-app, and update the docs that describe the change" + } +] diff --git a/.dev/browser/generate-fixture.mjs b/.dev/browser/generate-fixture.mjs new file mode 100644 index 0000000..5da14a3 --- /dev/null +++ b/.dev/browser/generate-fixture.mjs @@ -0,0 +1,254 @@ +// Generate public/data/icons.json from the REAL installed icon packs. +// +// The fixture used to be hand-authored and shipped package ids that match no +// Composer package at all -- ichava/ui-icons, ichava/test-icons, +// ichava/icons-bundle, ichava/color-icons, ichava/illustrations. Two of them were +// also the product's default filter, so the shipped default referenced packages +// that cannot be installed. Generating from the packs keeps the dev fixture and +// production honest about the same ids, categories and variants. +// +// Bounded on purpose: the real corpus is ~139,000 SVGs. This samples a +// deterministic spread per pack so the dev app exercises every archetype +// (currentColor stroke icons, own-colour flags, inline-style metronic, gradients) +// without shipping 118 MB. +// +// node scripts/generate-fixture.mjs [--per-pack 40] [--date YYYY-MM-DD] +// [--workspace DIR] [--public DIR] +// +// --workspace is the directory holding the pack checkouts (default: this package's +// parent) and --public the directory receiving data/icons.json and assets/ (default: +// public/). Both exist so the blocked-set test can run the real generator on a +// synthetic tree. +// +// Packs in scripts/blocked-icon-sets.mjs (Metronic, bundled) are skipped whole: never +// sampled, never listed in `packages`, never counted in `total_ecosystem`. They are +// private, so the fixture describes only what may be shown, the way the landing page +// counts. +// +// Output is deterministic for a given input tree: packs sorted, icons sorted, +// even-stride sampling, no timestamps beyond the explicit --date. Rerunning +// without a tree change produces no diff. + +import { readdirSync, readFileSync, statSync, existsSync, mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs'; +import { join, dirname, basename, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { blockedReason } from './blocked-icon-sets.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +// The repository root: this moved from react-icon-browser's scripts/ to .dev/browser/ (2026-11-12). +const APP = join(HERE, '..', '..'); + +const argv = process.argv.slice(2); +const argOf = (name, fallback) => { + const i = argv.indexOf(`--${name}`); + return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback; +}; +const WORKSPACE = argOf('workspace', join(APP, '..')); +const PUBLIC = argOf('public', join(HERE, 'public')); +const PER_PACK = Number(argOf('per-pack', '40')); +const GENERATED = argOf('date', new Date().toISOString().slice(0, 10)); + +/** The packs to sample, in display order. A pack absent from disk is skipped, not faked. */ +const PACKS = ['icon-sets-tabler', 'icon-sets-metronic', 'icon-sets-flag', 'icon-sets-bundled', 'icon-sets-twemoji', 'icon-sets-openmoji']; +// These are DIRECTORY names under ../, not composer names. They moved in the +// 2026-09-21 rebrand, and a stale entry here is silent: a pack absent from disk +// is skipped with a warning and the fixture regenerates smaller, not empty-and-loud. + +/** + * Icons that must always survive sampling, by pack. + * + * Even-stride sampling gives good spread but no stability: `home` fell out of a + * 40-icon tabler sample and took two e2e specs with it. Anchors keep the fixture + * addressable -- tests and the landing page's pack samples can name an icon and rely + * on it being there across regenerations. + * + * These are also the names ichava/react-landingpage's `public/data/landing*.json` + * references; that package carries a copy of this fixture's `icons.json`. + */ +const ANCHORS = { + 'ichava/icon-sets-tabler': ['home', 'heart', 'settings', 'camera', 'rocket', 'search', 'code', 'terminal', 'database', 'cloud', 'bolt', 'shield'], + 'ichava/icon-sets-flag': ['us', 'gb', 'ke', 'tz'], +}; + +const ASSET_ROOT = join(PUBLIC, 'assets'); +const OUT = join(PUBLIC, 'data', 'icons.json'); + +function walkSvgs(dir) { + const out = []; + if (!existsSync(dir)) return out; + for (const entry of readdirSync(dir).sort()) { + if (entry.startsWith('.')) continue; + const p = join(dir, entry); + if (statSync(p).isDirectory()) out.push(...walkSvgs(p)); + else if (entry.endsWith('.svg')) out.push(p); + } + return out; +} + +/** Even-stride sample: better category and name spread than the first N alphabetically. */ +function sample(list, n) { + if (list.length <= n) return list; + const stride = list.length / n; + const out = []; + for (let i = 0; i < n; i++) out.push(list[Math.floor(i * stride)]); + return out; +} + +const viewBoxOf = (svg) => /viewBox\s*=\s*["']([^"']+)["']/i.exec(svg)?.[1] ?? '0 0 24 24'; + +/** + * Does this icon carry its own colour, or does it inherit? + * + * An own-colour icon must render as an , not a CSS mask, or its fills are + * lost. Detection mirrors the runtime rule: an explicit paint that is not + * `currentColor`/`none`/`inherit`, a gradient, or an inline style that paints. + */ +function hasOwnColor(svg) { + if (/<(linearGradient|radialGradient|image)\b/i.test(svg)) return true; + if (/style\s*=\s*["'][^"']*(fill|stroke)\s*:/i.test(svg)) return true; + for (const m of svg.matchAll(/\b(fill|stroke)\s*=\s*["']([^"']*)["']/gi)) { + const v = m[2].trim().toLowerCase(); + if (!v || v === 'none' || v === 'currentcolor' || v === 'inherit' || v === 'transparent') continue; + if (v.startsWith('url(')) return true; + return true; + } + return false; +} + +const slug = (s) => s.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase(); + +let nextId = 1; +const packages = []; +const icons = []; +let totalEcosystem = 0; + +for (const packDir of PACKS) { + const root = join(WORKSPACE, packDir); + const cfgPath = join(root, 'resources', 'assets', 'svg', 'config.json'); + if (!existsSync(cfgPath)) { + console.warn(` skip ${packDir}: no config.json`); + continue; + } + const cfg = JSON.parse(readFileSync(cfgPath, 'utf8')); + const id = cfg.package?.name; + if (!id) { + console.warn(` skip ${packDir}: config.json declares no package.name`); + continue; + } + + // A blocked pack is excluded whole, before anything is read or counted. + const blocked = blockedReason(id); + if (blocked) { + // Clear any assets an earlier run copied, or they ship beside a catalog that no longer names them. + rmSync(join(ASSET_ROOT, slug(basename(packDir))), { recursive: true, force: true }); + console.log(` skip ${id}: blocked (${blocked}); not sampled, listed or counted`); + continue; + } + + const filesRoot = join(root, 'resources', 'assets', 'svg', 'files'); + const all = walkSvgs(filesRoot); + totalEcosystem += all.length; + + // A top-level directory is a VARIANT when the pack declares it as one, and a + // CATEGORY otherwise. tabler uses outline/filled variants; metronic uses + // flags/illustrations/file-types categories; bundled uses one directory per + // upstream set. Guessing from the layout alone gets three of the four wrong. + const declaredVariants = new Set(Object.keys(cfg.metadata?.data?.variants ?? {})); + const declaredCategories = new Set(Object.keys(cfg.metadata?.data?.categories ?? {})); + const defaultVariant = Object.entries(cfg.metadata?.data?.variants ?? {}).find(([, v]) => v?.default)?.[0] ?? 'default'; + + if (all.length === 0) { + // Record the pack as present but not loaded rather than silently omitting it. + // A pack with zero assets reporting "fine" is exactly the false-clean that hid + // emoji-sets shipping no icons at all. + packages.push({ id, label: basename(packDir), description: cfg.package?.description ?? '', count: 0, installed: true, loaded: false }); + console.warn(` ${id}: 0 SVGs on disk — recorded as installed but not loaded`); + continue; + } + + // Anchors first, then fill the remainder with an even-stride spread over what is + // left. Sorted at the end, so the output stays deterministic either way. + const wanted = new Set(ANCHORS[id] ?? []); + const anchored = all.filter((p) => wanted.has(basename(p, '.svg'))); + const missingAnchors = [...wanted].filter((n) => !anchored.some((p) => basename(p, '.svg') === n)); + if (missingAnchors.length) console.warn(` ${id}: anchors not found on disk: ${missingAnchors.join(', ')}`); + const rest = all.filter((p) => !anchored.includes(p)); + const picked = [...anchored, ...sample(rest, Math.max(0, PER_PACK - anchored.length))]; + const packSlug = slug(basename(packDir)); + const assetDir = join(ASSET_ROOT, packSlug); + rmSync(assetDir, { recursive: true, force: true }); + + for (const abs of picked) { + const rel = relative(filesRoot, abs); + const segments = rel.split('/'); + const top = segments.length > 1 ? segments[0] : ''; + const name = basename(abs, '.svg'); + + const isVariantDir = declaredVariants.has(top); + const variant = isVariantDir ? top : defaultVariant; + let category = isVariantDir ? 'general' : top || 'general'; + if (!isVariantDir && !declaredCategories.has(category) && declaredCategories.size) { + // A directory that is neither a declared variant nor a declared category is an + // upstream set name (bundled-icons). Keep it as the category: it is the axis a + // user actually browses by. + category = top; + } + const sub = segments.length > 2 ? segments[1] : null; + // What the SVG is, the way core resolves it: the category's `kind`, else the pack's, + // else an icon. Illustrations are a top-level kind of their own in the browser. + const kind = cfg.metadata?.data?.categories?.[category]?.kind ?? cfg.package?.kind ?? 'icon'; + + const svg = readFileSync(abs, 'utf8'); + const destRel = join(packSlug, ...segments); + const dest = join(ASSET_ROOT, destRel); + mkdirSync(dirname(dest), { recursive: true }); + copyFileSync(abs, dest); + + icons.push({ + id: nextId++, + package: id, + name, + category, + sub, + variant, + kind, + svgContent: null, + svgUrl: `assets/${destRel}`, + viewBox: viewBoxOf(svg), + // The path Laravel resolves (`vendor/package::`), as IconResource sends it. + // The snippet strings stay empty: the browser builds them from the path, and the + // ones written here before named tags and helpers that do not exist. + path: `${id}::${rel.replace(/\.svg$/, '')}`, + bladeClean: '', + bladeGeneric: '', + helper: '', + tags: [], + keywords: [], + ownColor: hasOwnColor(svg), + }); + } + + packages.push({ + id, + label: basename(packDir), + description: cfg.package?.description ?? '', + count: all.length, + installed: true, + loaded: true, + }); + console.log(` ${id}: sampled ${picked.length} of ${all.length}`); +} + +icons.sort((a, b) => a.package.localeCompare(b.package) || a.category.localeCompare(b.category) || a.name.localeCompare(b.name)); +icons.forEach((icon, i) => { + icon.id = i + 1; +}); +packages.sort((a, b) => a.id.localeCompare(b.id)); + +mkdirSync(dirname(OUT), { recursive: true }); +writeFileSync(OUT, JSON.stringify({ meta: { total_ecosystem: totalEcosystem, generated: GENERATED }, packages, icons }, null, 2) + '\n'); + +const ownColor = icons.filter((i) => i.ownColor).length; +console.log(`\nwrote ${icons.length} icons from ${packages.filter((p) => p.loaded).length} packs -> public/data/icons.json`); +console.log(` ecosystem total: ${totalEcosystem.toLocaleString('en-US')} SVGs`); +console.log(` own-colour icons: ${ownColor} (render via , not a CSS mask)`); diff --git a/.dev/browser/linux.sh b/.dev/browser/linux.sh new file mode 100644 index 0000000..919fa63 --- /dev/null +++ b/.dev/browser/linux.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Runs a Playwright suite on this machine inside the same image CI uses, so its pixels and +# its behaviour match CI's without a push and a 5-8 minute wait (Sim, 2026-10-28). +# +# npm run visual:linux regenerate the visual baselines, print what moved +# npm run visual:linux:check compare against the committed baselines, change nothing +# npm run e2e:linux run the behavioural E2E suite +# +# Needs Docker. Arguments after the mode go to `playwright test` (a spec file, --grep ...). +# The image is pinned here once; check:playwright-image fails if it drifts from ci.yml's +# visual job, the seed workflow or @playwright/test. +set -eu + +IMAGE=mcr.microsoft.com/playwright:v1.65.0-noble +mode=${1:?usage: scripts/linux.sh visual-update|visual-check|e2e [playwright args]} +shift + +if ! command -v docker >/dev/null 2>&1; then + echo "scripts/linux.sh: Docker is not installed or not on PATH; CI's image needs it." >&2 + exit 127 +fi + +exec docker run --rm --ipc=host -v "$PWD":/src "$IMAGE" sh /src/scripts/visual-linux.sh "$mode" "$@" diff --git a/.dev/browser/prepaint.d.mts b/.dev/browser/prepaint.d.mts new file mode 100644 index 0000000..4a54cb1 --- /dev/null +++ b/.dev/browser/prepaint.d.mts @@ -0,0 +1,10 @@ +/** Where a surface's brand suffix goes in the fragment ("Browser" in the app). */ +export const SUFFIX_SLOT: string; +/** The prepaint fragment without its leading documentation comment, its suffix slot filled. */ +export function prepaintFragment(opts?: { suffix?: string }): string; +/** `html` with `fragment` inserted straight after the opening tag. */ +export function insertAfterBody(html: string, fragment: string): string; +/** Vite plugin: inline the prepaint into the listed pages (default index.html). */ +export function injectPrepaint(opts?: { pages?: string[]; suffix?: string }): import('vite').Plugin; +/** Vite plugin: emit the fragment as `prepaint.html` beside the library bundle. */ +export function emitPrepaint(opts?: { suffix?: string }): import('vite').Plugin; diff --git a/.dev/browser/prepaint.mjs b/.dev/browser/prepaint.mjs new file mode 100644 index 0000000..db613d2 --- /dev/null +++ b/.dev/browser/prepaint.mjs @@ -0,0 +1,72 @@ +// The static prepaint loader (src/boot/prepaint.html): one source, two outputs. +// +// injectPrepaint() inlines it as the first child of in the standalone index.html, +// so the loader paints on the first HTML parse instead of after the bundle. +// emitPrepaint() ships it as dist/lib/prepaint.html for hosts (icon-browser's Blade +// layout, the demo) to inline the same way. + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Resolved from the package root when this module's URL is not a usable file: URL, which is +// the case under vitest's jsdom environment. Every script here runs from that root. +function sourcePath() { + try { + return fileURLToPath(new URL('../../resources/js/boot/prepaint.html', import.meta.url).href); + } catch { + return join(process.cwd(), 'resources/js/boot/prepaint.html'); + } +} + +/** Where a surface's brand suffix goes ("Browser" in the app), after the brand name. */ +export const SUFFIX_SLOT = ''; + +const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); + +/** + * The fragment without its leading documentation comment, with the suffix slot filled: the + * Bootsplash's `ichbs__suffix` span for a surface that has one, nothing otherwise. + */ +export function prepaintFragment({ suffix = '' } = {}) { + const raw = readFileSync(sourcePath(), 'utf8').replace(/^\s*\s*/, '').trim(); + if (!raw.includes(SUFFIX_SLOT)) throw new Error('prepaint: the fragment has no suffix slot'); + return raw.replace(SUFFIX_SLOT, suffix ? `${escapeHtml(suffix)}` : ''); +} + +/** Insert `fragment` straight after the opening tag; throws when there is none. */ +export function insertAfterBody(html, fragment) { + // Comments are blanked to the same length first: index.html's header comment mentions + // "", and the first build injected the loader inside that comment, where it never + // rendered. + const masked = html.replace(//g, (c) => ' '.repeat(c.length)); + const m = /]*>/i.exec(masked); + if (!m) throw new Error('prepaint: no tag to inject into'); + const at = m.index + m[0].length; + return `${html.slice(0, at)}\n${fragment}\n${html.slice(at)}`; +} + +/** Vite plugin: inline the prepaint into index.html (dev and build). */ +export function injectPrepaint({ pages = ['index.html'], suffix = '' } = {}) { + return { + name: 'ichava:prepaint-inject', + transformIndexHtml: { + order: 'pre', + handler(html, ctx) { + const page = ctx.filename.split(/[\\/]/).pop(); + return pages.includes(page) ? insertAfterBody(html, prepaintFragment({ suffix })) : html; + }, + }, + }; +} + +/** Vite plugin: emit the fragment as `prepaint.html` beside the library bundle. */ +export function emitPrepaint({ suffix = '' } = {}) { + return { + name: 'ichava:prepaint-emit', + apply: 'build', + generateBundle() { + this.emitFile({ type: 'asset', fileName: 'prepaint.html', source: `${prepaintFragment({ suffix })}\n` }); + }, + }; +} diff --git a/.dev/browser/public-api.snapshot.json b/.dev/browser/public-api.snapshot.json new file mode 100644 index 0000000..5bf786c --- /dev/null +++ b/.dev/browser/public-api.snapshot.json @@ -0,0 +1,111 @@ +{ + "IchavaBrowser": "export declare function IchavaBrowser({ catalog, client, config, manageDocument, syncUrl, urlPrefix, store, locale }: IchavaBrowserProps): import(\"react\").JSX.Element;", + "IchavaBrowserProps": "export interface IchavaBrowserProps {\n catalog?: Catalog;\n client?: IconCatalog;\n config?: AppConfig | null;\n manageDocument?: boolean;\n syncUrl?: boolean;\n urlPrefix?: string;\n store?: BrowserStoreHook;\n locale?: Locale;\n}", + "mountIchavaBrowser": "export declare function mountIchavaBrowser(el: HTMLElement, opts: MountOptions): Root;", + "browser": "export declare function browser(): BrowserBuilder;", + "MountOptions": "export interface MountOptions {\n catalog?: Catalog;\n client?: IconCatalog;\n config?: AppConfig | null;\n manageDocument?: boolean;\n syncUrl?: boolean;\n urlPrefix?: string;\n store?: BrowserStoreHook;\n persistKey?: string | null;\n locale?: Locale;\n}", + "BrowserBuilder": "export declare class BrowserBuilder {\n constructor(ports: BrowserBuilderPorts, state?: State);\n catalog(catalog: Catalog): BrowserBuilder;\n rest(client: IconCatalog): BrowserBuilder;\n config(config: AppConfig | null): BrowserBuilder;\n persist(key: string): BrowserBuilder;\n ephemeral(): BrowserBuilder;\n store(store: BrowserStoreHook): BrowserBuilder;\n standalone(): BrowserBuilder;\n embedded(): BrowserBuilder;\n syncUrl(prefix?: string): BrowserBuilder;\n locale(locale: Locale): BrowserBuilder;\n toOptions(): BuilderMountOptions;\n toProps(): IchavaBrowserProps;\n mount(el: HTMLElement): BrowserHandle;\n}", + "BrowserHandle": "export interface BrowserHandle {\n readonly store: BrowserStoreHook;\n unmount(): void;\n [Symbol.dispose](): void;\n}", + "BrowserBuilderPorts": "export interface BrowserBuilderPorts {\n mount(el: HTMLElement, options: BuilderMountOptions): {\n unmount(): void;\n };\n createStore(options: {\n persistKey?: string | null;\n }): BrowserStoreHook;\n}", + "createCatalog": "export declare function createCatalog(kind: DataSourceKind, opts: {\n catalog?: Catalog;\n base?: string;\n}): IconCatalog;", + "StaticCatalog": "export declare class StaticCatalog implements IconCatalog {\n constructor(catalog: Catalog);\n listIcons(params: ListParams): Promise>;\n getIcon(id: IconId): Promise;\n getSvg(icon: Icon, signal?: AbortSignal): Promise;\n getFilters(): Promise;\n getStatistics(): Promise>;\n getTree(): Promise;\n getRelated(icon: Icon, limit?: number): Promise;\n getVariantCounts(params: ListParams): Promise>;\n getKindCounts(params: ListParams): Promise>;\n}", + "RestCatalog": "export declare class RestCatalog implements IconCatalog {\n constructor(base?: string);\n static at(base: string): RestCatalog;\n get transport(): HttpTransport;\n static fromApi(api: {\n base: string | null;\n endpoints: RestEndpoints | unknown[];\n }): RestCatalog;\n get endpoints(): RestEndpoints | null;\n withTransport(transport: HttpTransport): RestCatalog;\n withEndpoints(endpoints: RestEndpoints | unknown[]): RestCatalog;\n csrf(strategy: CsrfStrategy): RestCatalog;\n timeout(ms: number): RestCatalog;\n credentials(mode: RequestCredentials): RestCatalog;\n listIcons(params: ListParams, signal?: AbortSignal): Promise>;\n getIcon(id: IconId, signal?: AbortSignal): Promise;\n getSvg(icon: Icon, signal?: AbortSignal): Promise;\n getFilters(signal?: AbortSignal): Promise;\n getStatistics(signal?: AbortSignal): Promise>;\n getTree(signal?: AbortSignal): Promise;\n getTreeFor(params: ListParams, signal?: AbortSignal): Promise;\n getCatalogMeta(signal?: AbortSignal): Promise;\n getRelated(icon: Icon, limit?: number, signal?: AbortSignal): Promise;\n getVariantCounts(_params: ListParams, signal?: AbortSignal): Promise>;\n getKindCounts(params: ListParams, signal?: AbortSignal): Promise>;\n}", + "IconCatalog": "export interface IconCatalog {\n listIcons(params: ListParams, signal?: AbortSignal): Promise>;\n getIcon(id: IconId, signal?: AbortSignal): Promise;\n getSvg(icon: Icon, signal?: AbortSignal): Promise;\n getFilters(signal?: AbortSignal): Promise;\n getStatistics(signal?: AbortSignal): Promise>;\n getTree(signal?: AbortSignal): Promise;\n getRelated(icon: Icon, limit?: number, signal?: AbortSignal): Promise;\n getVariantCounts(params: ListParams, signal?: AbortSignal): Promise>;\n getKindCounts?(params: ListParams, signal?: AbortSignal): Promise>;\n getTreeFor?(params: ListParams, signal?: AbortSignal): Promise;\n getCatalogMeta?(signal?: AbortSignal): Promise;\n}", + "ListParams": "export interface ListParams {\n search?: string;\n packages?: string[];\n categories?: string[];\n subs?: string[];\n variants?: string[];\n page?: number;\n perPage?: number;\n sortBy?: 'name' | 'package' | 'category' | 'created_at';\n sortDirection?: 'asc' | 'desc';\n kind?: IconKind;\n tags?: string[];\n keywords?: string[];\n language?: string;\n}", + "DataSourceKind": "export type DataSourceKind = 'static' | 'rest';", + "getBrowserId": "export declare function getBrowserId(): string;", + "LARAVEL_SANCTUM_PRESET": "LARAVEL_SANCTUM_PRESET: TransportConfig", + "HttpError": "export declare class HttpError extends Error {\n status: number;\n detail: string;\n constructor(status: number, detail: string);\n}", + "RequestAbortedError": "export declare class RequestAbortedError extends Error {\n reason: 'timeout' | 'caller';\n constructor(reason: 'timeout' | 'caller');\n}", + "HttpTransport": "export declare class HttpTransport {\n constructor(config?: TransportConfig);\n get config(): Readonly;\n with(patch: TransportPatch): HttpTransport;\n initCsrf(): Promise;\n request(path: string, opts?: RequestOpts): Promise;\n}", + "TransportConfig": "export interface TransportConfig {\n baseUrl: string;\n credentials: RequestCredentials;\n timeoutMs: number;\n csrf: {\n strategy: CsrfStrategy;\n metaName: string;\n cookieName: string;\n headerName: string;\n endpoint: string;\n };\n}", + "CsrfStrategy": "export type CsrfStrategy = 'none' | 'meta' | 'cookie' | 'sanctum';", + "IconRepository": "export declare class IconRepository {\n constructor(catalog: Catalog);\n filtered(p: ListParams): Icon[];\n page(p: ListParams): PageResult;\n byId(id: IconId): Icon | undefined;\n related(icon: Icon, limit?: number): Icon[];\n filters(): Filters;\n kindCounts(p: ListParams): Record;\n variantCounts(p: ListParams): Map;\n categoryTree(p: ListParams): CategoryGroup[];\n}", + "Catalog": "export interface Catalog {\n meta: {\n total_ecosystem: number;\n generated: string;\n };\n packages: IconPackage[];\n icons: Icon[];\n}", + "SvgFidelity": "export declare class SvgFidelity {\n resolve(icon: Icon, color: string | null): RenderStrategy;\n toStyle(s: RenderStrategy, size: number): StyleObject;\n}", + "fidelity": "fidelity: SvgFidelity", + "assetUrl": "export declare function assetUrl(file: string): string;", + "svgDataUrl": "export declare function svgDataUrl(svg: string): string;", + "SnippetFactory": "export declare class SnippetFactory {\n register(format: string, builder: SnippetBuilder): this;\n supports(format: string, config?: Partial | null): boolean;\n needsSvg(format: string, config?: Partial | null): boolean;\n build(format: SnippetFormat, icon: Icon, o: SnippetOptions, svgBody?: string | null): string;\n svgFromParts(icon: Icon, parts: {\n body: string;\n viewBox: string;\n }, o: SnippetOptions): string;\n}", + "snippets": "snippets: SnippetFactory", + "snippetTabs": "export declare function snippetTabs(config?: Partial | null, factory?: SnippetFactory): {\n id: SnippetFormat;\n label: string;\n builtin: boolean;\n}[];", + "snippetTokens": "export declare function snippetTokens(icon: Icon, o: SnippetOptions, svg?: string): Record;", + "renderTemplate": "export declare function renderTemplate(template: string, tokens: Record): string;", + "iconPath": "export declare function iconPath(icon: Icon): string;", + "svgToJsx": "export declare function svgToJsx(svg: string): string;", + "SNIPPET_TABS": "SNIPPET_TABS: {\n id: BuiltinSnippetFormat;\n label: string;\n}[]", + "BUILTIN_SNIPPET_FORMATS": "BUILTIN_SNIPPET_FORMATS: readonly [\n 'svg',\n 'motion',\n 'helper',\n 'blade',\n 'bladeGeneric',\n 'livewire',\n 'alpine',\n 'vue',\n 'react',\n 'svelte',\n 'webcomponent',\n 'reactNative',\n 'angular',\n 'solid',\n 'preact',\n 'astro',\n 'qwik',\n 'ember',\n 'elm',\n 'api',\n 'name'\n]", + "BuiltinSnippetFormat": "export type BuiltinSnippetFormat = (typeof BUILTIN_SNIPPET_FORMATS)[number];", + "SnippetBuilder": "export type SnippetBuilder = (ctx: SnippetContext) => string;", + "SnippetContext": "export interface SnippetContext {\n icon: Icon;\n options: SnippetOptions;\n tokens: Record;\n}", + "SnippetFormat": "export type SnippetFormat = BuiltinSnippetFormat | (string & {});", + "SnippetOptions": "export interface SnippetOptions {\n size: number;\n unit: string;\n color: string | null;\n strokeWidth?: number;\n baseUrl?: string;\n motion?: SnippetMotion;\n config?: Partial | null;\n}", + "SnippetConfig": "export interface SnippetConfig {\n tabs: string[];\n labels: Record;\n templates: Record;\n className: string;\n apiOrigin: string;\n svgEndpoint?: string;\n}", + "iconRef": "export declare function iconRef(icon: Icon): string;", + "IconId": "export type IconId = number;", + "IconVariant": "export type IconVariant = string;", + "IconKind": "export type IconKind = 'icon' | 'illustration';", + "RawIcon": "export interface RawIcon {\n id: number;\n package: string;\n name: string;\n category: string;\n sub?: string;\n variant: string;\n kind?: IconKind;\n path?: string;\n icon_path?: string;\n file_path?: string;\n svg_content?: string | null;\n svg_url?: string;\n viewbox?: string;\n width?: number | string | null;\n height?: number | string | null;\n blade_clean?: string;\n blade_generic?: string;\n helper?: string;\n snippets?: Record;\n set?: string;\n tags?: string[];\n keywords?: string[];\n created_at?: string;\n updated_at?: string;\n}", + "Icon": "export interface Icon {\n id: IconId;\n package: string;\n name: string;\n category: string;\n sub?: string;\n variant: IconVariant;\n kind?: IconKind;\n svgContent: string | null;\n svgUrl: string | null;\n viewBox: string;\n bladeClean: string;\n bladeGeneric: string;\n helper: string;\n path?: string;\n snippets?: Record;\n tags: string[];\n keywords: string[];\n ownColor: boolean;\n}", + "IconPackage": "export interface IconPackage {\n id: string;\n label: string;\n description?: string;\n count: number;\n installed: boolean;\n loaded: boolean;\n}", + "Term": "export interface Term {\n id: number | string;\n name: string;\n label: string;\n package?: string;\n count: number;\n sub?: string;\n}", + "Filters": "export interface Filters {\n packages: IconPackage[];\n categories: Term[];\n variants: Term[];\n}", + "PageMeta": "export interface PageMeta {\n total: number;\n perPage: number;\n currentPage: number;\n lastPage: number;\n from: number;\n to: number;\n groupBy?: string | null;\n}", + "Envelope": "export interface Envelope {\n success: boolean;\n data: T;\n message?: string;\n error?: string;\n meta?: Record;\n}", + "PageResult": "export interface PageResult {\n items: T[];\n total: number;\n page: number;\n perPage: number;\n lastPage: number;\n rangeStart: number;\n rangeEnd: number;\n}", + "svgHasOwnColors": "export declare function svgHasOwnColors(svg: string | null | undefined): boolean;", + "toIcon": "export declare function toIcon(raw: RawIcon): Icon;", + "useStore": "useStore: import(\"zustand\").UseBoundStore, \"persist\" | \"setState\"> & {\n setState(partial: BrowserStore | Partial | ((state: BrowserStore) => BrowserStore | Partial), replace?: false | undefined): unknown;\n setState(state: BrowserStore | ((state: BrowserStore) => BrowserStore), replace: true): unknown;\n persist: {\n setOptions: (options: Partial>) => void;\n clearStorage: () => void;\n rehydrate: () => Promise | void;\n hasHydrated: () => boolean;\n onHydrate: (fn: (state: BrowserStore) => void) => () => void;\n onFinishHydration: (fn: (state: BrowserStore) => void) => () => void;\n getOptions: () => Partial>;\n };\n}>", + "BrowserStore": "export interface BrowserStore {\n catalog: Catalog | null;\n filtersData: Filters | null;\n config: AppConfig | null;\n loading: boolean;\n activeWorkspaceId: string | null;\n setCatalog: (c: Catalog) => void;\n setFiltersData: (f: Filters) => void;\n setConfig: (c: AppConfig) => void;\n setActiveWorkspace: (id: string) => void;\n theme: Theme;\n accent: string;\n radius: number;\n scale: Scale;\n density: Density;\n reduceMotion: boolean;\n showLabels: boolean;\n adaptMonochrome: boolean;\n locale: Locale;\n setLocale: (l: Locale) => void;\n translationOverrides: TranslationOverrides;\n addTranslations: (data: TranslationOverrides) => void;\n setTheme: (t: Theme) => void;\n toggleTheme: () => void;\n setAccent: (hex: string) => void;\n setRadius: (n: number) => void;\n setScale: (s: Scale) => void;\n setDensity: (d: Density) => void;\n toggleReduceMotion: () => void;\n toggleLabels: () => void;\n toggleAdaptMonochrome: () => void;\n filters: FilterState;\n view: ViewMode;\n catQ: string;\n collapsedGroups: string[];\n expandedCats: string[];\n searchScope: SearchScope;\n packageQuery: string;\n sidebarOpen: boolean;\n setSearchScope: (s: SearchScope) => void;\n setPackageQuery: (v: string) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (v: boolean) => void;\n setSearch: (v: string) => void;\n clearSearch: () => void;\n togglePackage: (id: string) => void;\n selectAllPacks: (all: string[]) => void;\n clearPacks: () => void;\n toggleCat: (name: string) => void;\n toggleSub: (category: string, sub: string) => void;\n clearCats: () => void;\n setVariant: (v: string | null) => void;\n setSort: (k: SortKey) => void;\n toggleOrder: () => void;\n setPage: (n: number) => void;\n nextPage: (max: number) => void;\n prevPage: () => void;\n setPerPage: (n: number) => void;\n setView: (v: ViewMode) => void;\n setCatQ: (v: string) => void;\n toggleGroup: (pack: string) => void;\n toggleCatExpand: (key: string) => void;\n toggleExpandAll: (allGroups: string[], allCatKeys: string[]) => void;\n resetFilters: () => void;\n setKind: (k: IconKind) => void;\n toggleTag: (tag: string) => void;\n toggleKeyword: (keyword: string) => void;\n clearTerms: () => void;\n size: number;\n sizeUnit: SizeUnit;\n gridStroke: number;\n color: string | null;\n treatment: Treatment;\n copyFormat: CopyFormat;\n setSize: (n: number) => void;\n setSizeUnit: (u: SizeUnit) => void;\n setGridStroke: (n: number) => void;\n setColor: (c: string | null) => void;\n setTreatment: (t: Treatment) => void;\n setCopyFormat: (f: CopyFormat) => void;\n selection: IconId[];\n toggleSelect: (id: IconId) => void;\n clearSelection: () => void;\n favorites: IconId[];\n history: HistoryEntry[];\n collections: Collection[];\n toggleFavorite: (id: IconId) => void;\n pushHistory: (id: IconId, action: string) => void;\n clearHistory: () => void;\n createCollection: (name: string, icons?: readonly IconId[]) => void;\n renameCollection: (id: string, name: string) => void;\n removeCollection: (id: string) => void;\n addToCollection: (collId: string, iconId: IconId) => void;\n removeFromCollection: (collId: string, iconId: IconId) => void;\n recentCommands: string[];\n logCommand: (id: string, type?: 'action' | 'search' | 'navigation') => void;\n clearRecentCommands: () => void;\n libraryDeletions: number;\n deleteLibrary: () => void;\n layer: Layer;\n detailId: IconId | null;\n libTab: LibTab;\n librarySeed: IconId[];\n openLayer: (l: Layer) => void;\n closeLayer: () => void;\n openDetail: (id: IconId) => void;\n openLibrary: (tab: LibTab, opts?: {\n seed?: readonly IconId[];\n }) => void;\n toasts: {\n id: number;\n msg: string;\n icon: string;\n }[];\n showToast: (msg: string, icon?: string) => void;\n dismissToast: (id: number) => void;\n tasks: {\n id: number;\n label: string;\n }[];\n runTask: (label: string, work: () => Promise, opts?: {\n delayMs?: number;\n }) => Promise;\n ctx: {\n x: number;\n y: number;\n title?: string;\n items: ContextMenuItem[];\n } | null;\n openCtx: (x: number, y: number, items: ContextMenuItem[], title?: string) => void;\n closeCtx: () => void;\n setSelection: (ids: IconId[]) => void;\n auth: {\n status: AuthStatus;\n user: AuthUser | null;\n };\n authView: AuthView;\n authReason: string | null;\n authInitialized: boolean;\n openAuth: (view: Exclude, reason?: string) => void;\n closeAuth: () => void;\n signIn: (email: string, code: string) => boolean;\n createAccount: (p: {\n first: string;\n last: string;\n email: string;\n }) => void;\n signOut: () => void;\n notifications: AppNotification[];\n pushNotification: (n: {\n icon: string;\n text: string;\n }) => void;\n markNotificationsRead: () => void;\n clearNotifications: () => void;\n seedDemoNotifications: () => void;\n devtools: {\n open: boolean;\n tab: DevToolsTab;\n events: DevToolsEvent[];\n capturing: boolean;\n };\n devtoolsLayout: {\n dock: DevToolsDock;\n size: number;\n };\n setDevtoolsDock: (dock: DevToolsDock) => void;\n setDevtoolsSize: (size: number) => void;\n toggleDevtools: () => void;\n setDevToolsTab: (t: DevToolsTab) => void;\n pushDevEvent: (e: DevToolsEvent) => void;\n clearDevEvents: () => void;\n setCapturing: (v: boolean) => void;\n inviteOpen: boolean;\n accessCollectionId: string | null;\n sharedOpen: boolean;\n openShared: () => void;\n closeShared: () => void;\n openInvite: () => void;\n closeInvite: () => void;\n openAccess: (id: string) => void;\n closeAccess: () => void;\n shareCollection: (id: string, opts: {\n role: 'editor' | 'viewer';\n sharedWith: TeamRef[];\n }) => void;\n storageDriver: StorageDriver;\n setStorageDriver: (d: StorageDriver) => void;\n tour: {\n active: boolean;\n step: number;\n };\n tourSeen: boolean;\n startTour: () => void;\n nextTourStep: () => void;\n prevTourStep: () => void;\n skipTour: () => void;\n endTour: () => void;\n consent: 'essential' | 'all' | null;\n setConsent: (c: 'essential' | 'all') => void;\n boot: BootStateSlice;\n setBoot: (b: Partial) => void;\n confirm: ConfirmSpec | null;\n openConfirm: (c: ConfirmSpec) => void;\n closeConfirm: () => void;\n factoryReset: () => void;\n}", + "AppConfig": "export interface AppConfig {\n meta: {\n version: string;\n author: string;\n sets: number;\n license: string;\n url: string;\n };\n seo: {\n title: string;\n description: string;\n ogImage?: string;\n canonical?: string;\n };\n env: AppEnv | null;\n debug: {\n enabled: boolean | null;\n logLevel: LogLevel;\n };\n features: FeatureFlags;\n brand: {\n name: string;\n suffix: string;\n tagline: string;\n };\n user: {\n mode: string;\n name: string;\n email: string;\n initials: string;\n plan: string;\n workspaces: Workspace[];\n };\n team: TeamMember[];\n ui: {\n defaultTheme: 'light' | 'dark';\n scale: ScaleT;\n density: 'compact' | 'comfy';\n accentOptions: string[];\n perPageOptions: number[];\n scaleOptions: {\n id: ScaleT;\n label: string;\n }[];\n baseFontSize: number;\n avatarPalette: string[];\n };\n defaults: {\n packages: string[];\n perPage: number;\n sortBy: 'name' | 'package' | 'category';\n sortOrder: 'asc' | 'desc';\n appearance: {\n theme: 'light' | 'dark' | 'system';\n accent: string;\n radius: number;\n scale: ScaleT;\n density: 'compact' | 'comfy';\n reduceMotion: boolean;\n showLabels: boolean;\n adaptMonochrome?: boolean;\n };\n render: {\n size: number;\n sizeUnit: 'px' | 'rem' | 'em';\n gridStroke: number;\n color: string | null;\n treatment: string;\n copyFormat: CopyFormat;\n };\n collections: {\n id: string;\n name: string;\n icons: number[];\n shared?: boolean;\n role?: 'editor' | 'viewer';\n sharedWith?: {\n initials: string;\n name: string;\n }[];\n }[];\n kind?: IconKind;\n };\n ranges: {\n size: {\n min: number;\n max: number;\n step: number;\n };\n stroke: {\n min: number;\n max: number;\n step: number;\n };\n radius: {\n min: number;\n max: number;\n };\n };\n limits: {\n historyCap: number;\n recentCommandsCap: number;\n paletteIconResults: number;\n logBuffer: number;\n svgMaxChars: number;\n svgMaxNodes: number;\n };\n toolbar: {\n sortOptions: {\n id: string;\n label: string;\n }[];\n treatments: {\n id: string;\n label: string;\n desc: string;\n }[];\n sizeUnits: string[];\n copyFormats: {\n id: CopyFormat;\n label: string;\n }[];\n exportFormats: {\n id: string;\n label: string;\n desc: string;\n icon?: string;\n }[];\n nativeExports?: {\n id: string;\n href: string;\n }[];\n };\n about: {\n description: string;\n creditsTitle: string;\n copyright: string;\n creditsMarqueeSec: number;\n credits: {\n name: string;\n author: string;\n license: string;\n }[];\n links: {\n label: string;\n href: string;\n }[];\n stats: {\n key: string;\n label: string;\n }[];\n };\n i18n: {\n locale: string;\n available: string[];\n endpoint?: string;\n };\n storageKeys: {\n persist: string;\n driver: string;\n paletteRecent: string;\n locale: string;\n consent?: string;\n };\n host: HostContext | null;\n boot: Record;\n snippets: SnippetConfig;\n storage: {\n frontendDriver: 'local' | 'session';\n laravelDriver?: string;\n };\n crossLinks: {\n landing: string | null;\n };\n}", + "resolveConfig": "export declare function resolveConfig(partial: PartialConfig | null | undefined): AppConfig;", + "CONFIG_DEFAULTS": "CONFIG_DEFAULTS: AppConfig", + "PartialConfig": "export type PartialConfig = DeepPartial;", + "TaskLoaderView": "export declare function TaskLoaderView({ label, more }: {\n label: string;\n more?: number;\n}): import(\"react\").JSX.Element;", + "TASK_LOADER_DELAY_MS": "TASK_LOADER_DELAY_MS = 2000", + "hostConfig": "export declare function hostConfig(host: HostContext): PartialConfig;", + "hostConfigFromProps": "export declare function hostConfigFromProps(props: {\n auth?: User | null;\n ichava?: Partial | null;\n} | null | undefined): PartialConfig;", + "loadHostConfig": "export declare function loadHostConfig(bootstrapUrl: string, transport?: HttpTransport): Promise;", + "loadHostContext": "export declare function loadHostContext(bootstrapUrl: string, transport?: HttpTransport): Promise;", + "hostFromInertia": "export declare function hostFromInertia(props: {\n auth?: User | null;\n ichava?: Partial | null;\n} | null | undefined): HostContext | null;", + "hostFromBootstrap": "export declare function hostFromBootstrap(doc: Bootstrap): HostContext;", + "featureOn": "export declare function featureOn(host: HostContext, name: FeatureName): boolean;", + "endpoint": "export declare function endpoint(host: HostContext, name: FeatureName, key: string): string | null;", + "fillEndpoint": "export declare function fillEndpoint(template: string, params: Readonly>): string;", + "isCompatible": "export declare function isCompatible(version: string, ours?: string): boolean;", + "restLibraryEndpoints": "export declare function restLibraryEndpoints(host: HostContext): {\n library: string;\n favorites: string | null;\n collections: string | null;\n history: string | null;\n commands: string | null;\n preferences: string | null;\n api: Readonly>>;\n} | null;", + "apiEndpoint": "export declare function apiEndpoint(host: HostContext | null | undefined, key: ApiEndpointKey): string | null;", + "hostApi": "export declare function hostApi(api: Partial | null | undefined): HostApi | null;", + "ABILITIES": "ABILITIES: {\n readonly admin: 'ichava.admin';\n readonly manageCache: 'ichava.manage-cache';\n}", + "HostContext": "export interface HostContext {\n source: HostSource;\n schemaVersion: string;\n compatible: boolean;\n locale: string | null;\n modes: Modes;\n user: User | null;\n features: Readonly>>;\n links: Links;\n storage: {\n tier: string | null;\n persistent: boolean | null;\n };\n defaultTheme: 'light' | 'dark' | null;\n perPage: number | null;\n api?: HostApi | null;\n kinds?: KindsInfo | null;\n limits?: {\n svgMaxNodes: number | null;\n svgMaxChars: number | null;\n } | null;\n crossLinks?: CrossLinks | null;\n}", + "FeatureName": "export type FeatureName = keyof Features;", + "HostApi": "export interface HostApi {\n version: string;\n base: string | null;\n endpoints: Readonly>>;\n}", + "ApiEndpointKey": "export type ApiEndpointKey = keyof Exclude;", + "HostIdentity": "export declare class HostIdentity implements IdentityProvider {\n constructor(user: HostUserData | null, links: HostAccountLinks, opts?: HostIdentityOptions);\n current(): IdentityUser | null;\n signIn(): IdentityUser | null;\n createAccount(): IdentityUser;\n beginSignIn(): boolean;\n signOut(): void;\n}", + "DemoIdentity": "export declare class DemoIdentity implements IdentityProvider {\n signIn(email: string, code: string, context: IdentityContext | null): IdentityUser | null;\n createAccount(p: {\n first: string;\n last: string;\n email: string;\n }): IdentityUser;\n signOut(): void;\n}", + "IdentityProvider": "export interface IdentityProvider {\n signIn(email: string, code: string, context: IdentityContext | null): IdentityUser | null;\n createAccount(person: {\n first: string;\n last: string;\n email: string;\n }): IdentityUser;\n signOut(): void;\n current?(): IdentityUser | null;\n beginSignIn?(view: 'signin' | 'create'): boolean;\n}", + "IdentityUser": "export interface IdentityUser {\n name: string;\n initials: string;\n email: string;\n plan: 'FREE' | 'PRO' | 'TEAM' | null;\n id?: string;\n avatarUrl?: string | null;\n abilities?: Readonly>;\n}", + "diffLibrary": "export declare function diffLibrary(prev: LibraryView, next: LibraryView): LibraryOp[];", + "RestLibrary": "export declare class RestLibrary {\n constructor(endpoints: LibraryEndpoints, transport: HttpTransport);\n serverId(id: string): string;\n load(): Promise;\n apply(op: LibraryOp): Promise<{\n from: string;\n to: string;\n } | void>;\n}", + "LibraryOp": "export type LibraryOp = {\n kind: 'favorite.add';\n iconId: IconId;\n} | {\n kind: 'favorite.remove';\n iconId: IconId;\n} | {\n kind: 'history.record';\n iconId: IconId;\n action: string;\n} | {\n kind: 'history.clear';\n} | {\n kind: 'collection.create';\n id: string;\n name: string;\n icons?: readonly IconId[];\n} | {\n kind: 'collection.rename';\n id: string;\n name: string;\n} | {\n kind: 'collection.delete';\n id: string;\n} | {\n kind: 'collection.add';\n id: string;\n iconId: IconId;\n} | {\n kind: 'collection.remove';\n id: string;\n iconId: IconId;\n} | {\n kind: 'command.log';\n command: string;\n} | {\n kind: 'command.clear';\n} | {\n kind: 'library.delete';\n};", + "LibraryView": "export interface LibraryView {\n favorites: readonly IconId[];\n history: readonly HistoryEntry[];\n collections: readonly Collection[];\n recentCommands?: readonly string[];\n libraryDeletions?: number;\n}", + "LibraryEndpoints": "export interface LibraryEndpoints {\n library: string;\n favorites: string | null;\n collections: string | null;\n history: string | null;\n commands: string | null;\n api?: Readonly>>;\n}", + "RestPreferences": "export declare class RestPreferences {\n constructor(url: string, transport: HttpTransport);\n static withEndpoints(endpoints: PreferenceEndpoints, transport: HttpTransport): RestPreferences;\n load(): Promise;\n save(p: HostPreferences): Promise;\n reset(): Promise;\n}", + "HostPreferences": "export interface HostPreferences {\n view?: 'grid' | 'list';\n size?: number;\n perPage?: number;\n sortBy?: 'name' | 'package' | 'category';\n sortOrder?: 'asc' | 'desc';\n}", + "PreferenceEndpoints": "export interface PreferenceEndpoints {\n show: string | null;\n update: string | null;\n reset: string | null;\n}", + "mergeConfig": "export declare function mergeConfig(base: T, over: unknown): T;", + "emptyReasonFor": "export declare function emptyReasonFor(page: PageResult, params: ListParams, catalogSize: number): EmptyReason;", + "ListedPage": "export type ListedPage = PageResult & {\n emptyReason?: EmptyReason;\n};", + "PAYLOAD_SCHEMA_VERSION": "PAYLOAD_SCHEMA_VERSION = \"1.8.0\"", + "HostBootstrap": "export type HostBootstrap = {\n schemaVersion: string;\n locale: string;\n modes: Modes;\n session: Session;\n features: Features;\n api: ApiManifest;\n kinds?: KindsInfo;\n limits?: Limits;\n crossLinks?: CrossLinks;\n};", + "HostSession": "export type HostSession = {\n schemaVersion: string;\n authenticated: boolean;\n user: User | null;\n tier: \"browser\" | \"session\" | \"database\";\n persistent: boolean;\n links: Links;\n};", + "HostUser": "export type HostUser = {\n id: string;\n name: string;\n avatarUrl: string | null;\n abilities: Record;\n};", + "HostFeature": "export type HostFeature = {\n enabled: boolean;\n reason: \"config\" | \"gate\" | \"driver\" | \"empty\" | null;\n endpoints: Record | unknown[];\n};", + "HostFeatures": "export type HostFeatures = {\n favorites: Feature;\n collections: Feature;\n history: Feature;\n commands: Feature;\n library: Feature;\n preferences: Feature;\n status: Feature;\n cacheAdmin: Feature;\n illustrations: Feature;\n searchLanguage: Feature;\n export: Feature;\n motion: Feature;\n themeToggle: Feature;\n presence: Feature;\n invites: Feature;\n sharing: Feature;\n notifications: Feature;\n workspaces: Feature;\n};", + "HostIchavaProp": "export type HostIchavaProp = {\n schemaVersion: string;\n locale: string;\n modes: Modes;\n features: Features;\n account: Links;\n storage: {\n tier: string | null;\n persistent: boolean | null;\n };\n prefix: string;\n perPage: number;\n defaultTheme: string;\n ui: {\n version: string | null;\n license: string;\n docsUrl: string | null;\n links: {\n label: string;\n href: string;\n }[];\n themeToggle: boolean;\n };\n snippets: Record | unknown[];\n routes: Record;\n kinds?: KindsInfo;\n limits?: Limits;\n crossLinks?: CrossLinks;\n};", + "HostApiManifest": "export type HostApiManifest = {\n version: \"v1\";\n base: string | null;\n endpoints: {\n session?: string;\n bootstrap?: string;\n status?: string;\n \"icons.list\"?: string;\n \"icons.show\"?: string;\n \"icons.svg\"?: string;\n \"icons.filters\"?: string;\n \"icons.tree\"?: string;\n \"icons.statistics\"?: string;\n \"icons.updateStatus\"?: string;\n \"packages.list\"?: string;\n \"packages.show\"?: string;\n \"terms.categories\"?: string;\n \"terms.variants\"?: string;\n \"terms.hierarchy\"?: string;\n \"library.show\"?: string;\n \"library.destroy\"?: string;\n \"favorites.list\"?: string;\n \"favorites.add\"?: string;\n \"favorites.remove\"?: string;\n \"favorites.toggle\"?: string;\n \"collections.list\"?: string;\n \"collections.create\"?: string;\n \"collections.show\"?: string;\n \"collections.rename\"?: string;\n \"collections.delete\"?: string;\n \"collections.addIcon\"?: string;\n \"collections.removeIcon\"?: string;\n \"history.list\"?: string;\n \"history.log\"?: string;\n \"history.clear\"?: string;\n \"commands.list\"?: string;\n \"commands.log\"?: string;\n \"commands.clear\"?: string;\n \"preferences.show\"?: string;\n \"preferences.update\"?: string;\n \"preferences.search\"?: string;\n \"preferences.filters\"?: string;\n \"preferences.reset\"?: string;\n \"cache.stats\"?: string;\n \"cache.clear\"?: string;\n \"cache.rebuild\"?: string;\n } | unknown[];\n};", + "HostSharedProps": "export type HostSharedProps = {\n auth: User | null;\n flash: {\n success: string | null;\n error: string | null;\n };\n preferences: Record;\n ichava: Ichava;\n};", + "EmptyReason": "export type EmptyReason = \"no_match\" | \"no_icons\" | null;", + "loadLocale": "export declare function loadLocale(locale: Locale): Promise;", + "loadAllLocales": "export declare function loadAllLocales(): Promise;" +} diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/ad.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/ad.svg new file mode 100644 index 0000000..dde82d2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/ad.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/ax.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/ax.svg new file mode 100644 index 0000000..cd4e76d --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/bs.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/bs.svg new file mode 100644 index 0000000..5b5aa4f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/bs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/cn.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/cn.svg new file mode 100644 index 0000000..d434529 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/dz.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/dz.svg new file mode 100644 index 0000000..03ff735 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/dz.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/fo.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/fo.svg new file mode 100644 index 0000000..72b85a8 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/fo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/gb.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/gb.svg new file mode 100644 index 0000000..7c2229d --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/gb.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/gq.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/gq.svg new file mode 100644 index 0000000..e431354 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/gq.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/il.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/il.svg new file mode 100644 index 0000000..1fed005 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/ke.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/ke.svg new file mode 100644 index 0000000..f00f64b --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/ke.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/kp.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/kp.svg new file mode 100644 index 0000000..10a1fea --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/kp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/mc.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/mc.svg new file mode 100644 index 0000000..94d45fb --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/mc.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/mu.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/mu.svg new file mode 100644 index 0000000..3f07eef --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/mu.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/nz.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/nz.svg new file mode 100644 index 0000000..52cf49e --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/nz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/qa.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/qa.svg new file mode 100644 index 0000000..3153e07 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/qa.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/si.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/si.svg new file mode 100644 index 0000000..033fcd3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/si.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/tg.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/tg.svg new file mode 100644 index 0000000..fbe5b38 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/tg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/tz.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/tz.svg new file mode 100644 index 0000000..03dff11 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/tz.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/us.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/us.svg new file mode 100644 index 0000000..f6c25cf --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/us.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/1x1/uz.svg b/.dev/browser/public/assets/icon-sets-flag/1x1/uz.svg new file mode 100644 index 0000000..96b3572 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/1x1/uz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/ad.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/ad.svg new file mode 100644 index 0000000..bb72fe3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/ad.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/ax.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/ax.svg new file mode 100644 index 0000000..693d47d --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/bs.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/bs.svg new file mode 100644 index 0000000..27cd34f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/bs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/cn.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/cn.svg new file mode 100644 index 0000000..b1aca5a --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/dz.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/dz.svg new file mode 100644 index 0000000..921473c --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/dz.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/fo.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/fo.svg new file mode 100644 index 0000000..b3ee7ab --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/fo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/gb.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/gb.svg new file mode 100644 index 0000000..d53cb7b --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/gb.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/gq.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/gq.svg new file mode 100644 index 0000000..eb1d897 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/gq.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/il.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/il.svg new file mode 100644 index 0000000..25e2285 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/ke.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/ke.svg new file mode 100644 index 0000000..36fca27 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/ke.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/kp.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/kp.svg new file mode 100644 index 0000000..4c82478 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/kp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/mc.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/mc.svg new file mode 100644 index 0000000..ec65df5 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/mc.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/mu.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/mu.svg new file mode 100644 index 0000000..effd113 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/mu.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/nz.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/nz.svg new file mode 100644 index 0000000..5d98735 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/nz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/qa.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/qa.svg new file mode 100644 index 0000000..5f074d6 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/qa.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/si.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/si.svg new file mode 100644 index 0000000..2caec41 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/si.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/tg.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/tg.svg new file mode 100644 index 0000000..56dfe25 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/tg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/tz.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/tz.svg new file mode 100644 index 0000000..1df5266 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/tz.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/us.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/us.svg new file mode 100644 index 0000000..0870c38 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/us.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-flag/4x3/uz.svg b/.dev/browser/public/assets/icon-sets-flag/4x3/uz.svg new file mode 100644 index 0000000..e8bbf08 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-flag/4x3/uz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/activities/1st-place-medal.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/activities/1st-place-medal.svg new file mode 100644 index 0000000..0d4ce94 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/activities/1st-place-medal.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/flags/flag-burkina-faso.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/flags/flag-burkina-faso.svg new file mode 100644 index 0000000..f3e29c3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/flags/flag-burkina-faso.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/food-drink/coconut.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/food-drink/coconut.svg new file mode 100644 index 0000000..f86a5d9 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/food-drink/coconut.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/objects/sari.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/objects/sari.svg new file mode 100644 index 0000000..d57fd85 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/objects/sari.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/deaf-woman-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/deaf-woman-medium-dark-skin-tone.svg new file mode 100644 index 0000000..6ea28d8 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/deaf-woman-medium-dark-skin-tone.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone.svg new file mode 100644 index 0000000..fa21811 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/man-in-steamy-room-medium-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/man-in-steamy-room-medium-skin-tone.svg new file mode 100644 index 0000000..4496951 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/man-in-steamy-room-medium-skin-tone.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/mechanic-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/mechanic-medium-dark-skin-tone.svg new file mode 100644 index 0000000..4682fb1 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/mechanic-medium-dark-skin-tone.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/person-golfing-medium-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/person-golfing-medium-light-skin-tone.svg new file mode 100644 index 0000000..cbd0480 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/person-golfing-medium-light-skin-tone.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/police-officer.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/police-officer.svg new file mode 100644 index 0000000..a49feeb --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/police-officer.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-dancing-medium-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-dancing-medium-light-skin-tone.svg new file mode 100644 index 0000000..fca0071 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-dancing-medium-light-skin-tone.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-raising-hand-medium-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-raising-hand-medium-light-skin-tone.svg new file mode 100644 index 0000000..0669c0c --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/people-body/woman-raising-hand-medium-light-skin-tone.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/smileys-emotion/kiss-mark.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/smileys-emotion/kiss-mark.svg new file mode 100644 index 0000000..9be43b2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/smileys-emotion/kiss-mark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/travel-places/ferris-wheel.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/travel-places/ferris-wheel.svg new file mode 100644 index 0000000..8f460b6 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-black/travel-places/ferris-wheel.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/animals-nature/guide-dog.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/animals-nature/guide-dog.svg new file mode 100644 index 0000000..6ed9bfc --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/animals-nature/guide-dog.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/flags/flag-nigeria.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/flags/flag-nigeria.svg new file mode 100644 index 0000000..c6c76c3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/flags/flag-nigeria.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/objects/cigarette.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/objects/cigarette.svg new file mode 100644 index 0000000..6c0e37f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/objects/cigarette.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/construction-worker-medium-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/construction-worker-medium-skin-tone.svg new file mode 100644 index 0000000..171c297 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/construction-worker-medium-skin-tone.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/handshake-light-skin-tone-medium-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/handshake-light-skin-tone-medium-skin-tone.svg new file mode 100644 index 0000000..99d774c --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/handshake-light-skin-tone-medium-skin-tone.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-dark-skin-tone.svg new file mode 100644 index 0000000..6204888 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-dark-skin-tone.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-rowing-boat.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-rowing-boat.svg new file mode 100644 index 0000000..e52f86f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/man-rowing-boat.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/palm-down-hand-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/palm-down-hand-light-skin-tone.svg new file mode 100644 index 0000000..6377f1c --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/palm-down-hand-light-skin-tone.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/person-raising-hand-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/person-raising-hand-medium-dark-skin-tone.svg new file mode 100644 index 0000000..cb12ed6 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/person-raising-hand-medium-dark-skin-tone.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/technologist-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/technologist-dark-skin-tone.svg new file mode 100644 index 0000000..d16ba40 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/technologist-dark-skin-tone.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-in-motorized-wheelchair-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-in-motorized-wheelchair-medium-dark-skin-tone.svg new file mode 100644 index 0000000..4c0dbc2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-in-motorized-wheelchair-medium-dark-skin-tone.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-with-white-cane-facing-right-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-with-white-cane-facing-right-light-skin-tone.svg new file mode 100644 index 0000000..e539119 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/people-body/woman-with-white-cane-facing-right-light-skin-tone.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/symbols/japanese-discount-button.svg b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/symbols/japanese-discount-button.svg new file mode 100644 index 0000000..f8d322f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-openmoji/openmoji-color/symbols/japanese-discount-button.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/accessible.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/accessible.svg new file mode 100644 index 0000000..2f096b5 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/accessible.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/bolt.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/bolt.svg new file mode 100644 index 0000000..52c840f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/bolt.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/camera.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/camera.svg new file mode 100644 index 0000000..f76bade --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/camera.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/circle-number-9.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/circle-number-9.svg new file mode 100644 index 0000000..3d9c049 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/circle-number-9.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/cloud.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/cloud.svg new file mode 100644 index 0000000..00ada14 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/cloud.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/database.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/database.svg new file mode 100644 index 0000000..ba1ab3a --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/database.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/heart.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/heart.svg new file mode 100644 index 0000000..5dffbcf --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/heart.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/home.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/home.svg new file mode 100644 index 0000000..e2f7503 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/home.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/layout-dashboard.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/layout-dashboard.svg new file mode 100644 index 0000000..ddd7bfd --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/layout-dashboard.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/search.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/search.svg new file mode 100644 index 0000000..43849f6 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/settings.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/settings.svg new file mode 100644 index 0000000..248d203 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/settings.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/shield.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/shield.svg new file mode 100644 index 0000000..20e6082 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/shield.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/filled/swipe-right.svg b/.dev/browser/public/assets/icon-sets-tabler/filled/swipe-right.svg new file mode 100644 index 0000000..2b5ff52 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/filled/swipe-right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/arrow-ramp-right-3.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/arrow-ramp-right-3.svg new file mode 100644 index 0000000..e82d447 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/arrow-ramp-right-3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/bolt.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/bolt.svg new file mode 100644 index 0000000..c0fba72 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/bolt.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/bottle.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/bottle.svg new file mode 100644 index 0000000..2fde2b4 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/bottle.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/brand-stackshare.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/brand-stackshare.svg new file mode 100644 index 0000000..b4088ef --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/brand-stackshare.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/camera.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/camera.svg new file mode 100644 index 0000000..a3895b2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/camera.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/chair-director.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/chair-director.svg new file mode 100644 index 0000000..490b127 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/chair-director.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/cloud-cog.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/cloud-cog.svg new file mode 100644 index 0000000..e0a37e0 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/cloud-cog.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/cloud.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/cloud.svg new file mode 100644 index 0000000..9a3f3e8 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/cloud.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/code.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/code.svg new file mode 100644 index 0000000..618ee4d --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/code.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/database.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/database.svg new file mode 100644 index 0000000..0d60488 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/database.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/device-imac-off.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/device-imac-off.svg new file mode 100644 index 0000000..8e90a69 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/device-imac-off.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/eye-pin.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/eye-pin.svg new file mode 100644 index 0000000..ccde930 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/eye-pin.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/git-branch-check.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/git-branch-check.svg new file mode 100644 index 0000000..4e19636 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/git-branch-check.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/heart.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/heart.svg new file mode 100644 index 0000000..247ab75 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/heart.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/home.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/home.svg new file mode 100644 index 0000000..3113b80 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/home.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/join-straight.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/join-straight.svg new file mode 100644 index 0000000..909d0d3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/join-straight.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/map-pin-off.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/map-pin-off.svg new file mode 100644 index 0000000..9ef362f --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/map-pin-off.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/navigation-plus.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/navigation-plus.svg new file mode 100644 index 0000000..dd985da --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/navigation-plus.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/physiotherapist.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/physiotherapist.svg new file mode 100644 index 0000000..337c7de --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/physiotherapist.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/rocket.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/rocket.svg new file mode 100644 index 0000000..87489e2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/rocket.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/run-sprint.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/run-sprint.svg new file mode 100644 index 0000000..a172cfb --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/run-sprint.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/search.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/search.svg new file mode 100644 index 0000000..da6cf1e --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/search.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/settings.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/settings.svg new file mode 100644 index 0000000..adc36b0 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/settings.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/shield.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/shield.svg new file mode 100644 index 0000000..6ae173c --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/shield.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/square-letter-y.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/square-letter-y.svg new file mode 100644 index 0000000..33c2228 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/square-letter-y.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/terminal.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/terminal.svg new file mode 100644 index 0000000..b8242d2 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/terminal.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-tabler/outline/traffic-cone-off.svg b/.dev/browser/public/assets/icon-sets-tabler/outline/traffic-cone-off.svg new file mode 100644 index 0000000..edbfc81 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-tabler/outline/traffic-cone-off.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/activities/1st-place-medal.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/activities/1st-place-medal.svg new file mode 100644 index 0000000..a0c5109 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/activities/1st-place-medal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/flags/flag-burundi.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/flags/flag-burundi.svg new file mode 100644 index 0000000..9307dc0 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/flags/flag-burundi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/food-drink/croissant.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/food-drink/croissant.svg new file mode 100644 index 0000000..3ec6329 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/food-drink/croissant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/objects/shopping-cart.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/objects/shopping-cart.svg new file mode 100644 index 0000000..e4a4b32 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/objects/shopping-cart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/detective-medium-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/detective-medium-dark-skin-tone.svg new file mode 100644 index 0000000..3cf47c1 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/detective-medium-dark-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/kiss-woman-woman-medium-dark-skin-tone-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/kiss-woman-woman-medium-dark-skin-tone-light-skin-tone.svg new file mode 100644 index 0000000..536b9ae --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/kiss-woman-woman-medium-dark-skin-tone-light-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/man-kneeling-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/man-kneeling-light-skin-tone.svg new file mode 100644 index 0000000..dcfd6e3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/man-kneeling-light-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/merman.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/merman.svg new file mode 100644 index 0000000..09c8972 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/merman.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/person-juggling-medium-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/person-juggling-medium-light-skin-tone.svg new file mode 100644 index 0000000..1c05bc3 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/person-juggling-medium-light-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/scientist-medium-light-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/scientist-medium-light-skin-tone.svg new file mode 100644 index 0000000..dbbd603 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/scientist-medium-light-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-getting-haircut.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-getting-haircut.svg new file mode 100644 index 0000000..cbe2a48 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-getting-haircut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-wearing-turban-dark-skin-tone.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-wearing-turban-dark-skin-tone.svg new file mode 100644 index 0000000..0fce709 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/people-body/woman-wearing-turban-dark-skin-tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/icon-sets-twemoji/twemoji/symbols/heavy-dollar-sign.svg b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/symbols/heavy-dollar-sign.svg new file mode 100644 index 0000000..fefc7e4 --- /dev/null +++ b/.dev/browser/public/assets/icon-sets-twemoji/twemoji/symbols/heavy-dollar-sign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/anchor.svg b/.dev/browser/public/assets/tabler/anchor.svg new file mode 100644 index 0000000..fecb936 --- /dev/null +++ b/.dev/browser/public/assets/tabler/anchor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/archive.svg b/.dev/browser/public/assets/tabler/archive.svg new file mode 100644 index 0000000..a97907c --- /dev/null +++ b/.dev/browser/public/assets/tabler/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/award.svg b/.dev/browser/public/assets/tabler/award.svg new file mode 100644 index 0000000..eec88b2 --- /dev/null +++ b/.dev/browser/public/assets/tabler/award.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/battery.svg b/.dev/browser/public/assets/tabler/battery.svg new file mode 100644 index 0000000..393cf07 --- /dev/null +++ b/.dev/browser/public/assets/tabler/battery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/bell.svg b/.dev/browser/public/assets/tabler/bell.svg new file mode 100644 index 0000000..468a55d --- /dev/null +++ b/.dev/browser/public/assets/tabler/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/bluetooth.svg b/.dev/browser/public/assets/tabler/bluetooth.svg new file mode 100644 index 0000000..3feee51 --- /dev/null +++ b/.dev/browser/public/assets/tabler/bluetooth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/bolt.svg b/.dev/browser/public/assets/tabler/bolt.svg new file mode 100644 index 0000000..80bfc3a --- /dev/null +++ b/.dev/browser/public/assets/tabler/bolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/book.svg b/.dev/browser/public/assets/tabler/book.svg new file mode 100644 index 0000000..3f7cf7c --- /dev/null +++ b/.dev/browser/public/assets/tabler/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/bookmark.svg b/.dev/browser/public/assets/tabler/bookmark.svg new file mode 100644 index 0000000..92fa173 --- /dev/null +++ b/.dev/browser/public/assets/tabler/bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/box.svg b/.dev/browser/public/assets/tabler/box.svg new file mode 100644 index 0000000..0d92d43 --- /dev/null +++ b/.dev/browser/public/assets/tabler/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/briefcase.svg b/.dev/browser/public/assets/tabler/briefcase.svg new file mode 100644 index 0000000..57b53d5 --- /dev/null +++ b/.dev/browser/public/assets/tabler/briefcase.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/bulb.svg b/.dev/browser/public/assets/tabler/bulb.svg new file mode 100644 index 0000000..e3a92f2 --- /dev/null +++ b/.dev/browser/public/assets/tabler/bulb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/calendar.svg b/.dev/browser/public/assets/tabler/calendar.svg new file mode 100644 index 0000000..595b9ba --- /dev/null +++ b/.dev/browser/public/assets/tabler/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/camera.svg b/.dev/browser/public/assets/tabler/camera.svg new file mode 100644 index 0000000..35acd51 --- /dev/null +++ b/.dev/browser/public/assets/tabler/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/car.svg b/.dev/browser/public/assets/tabler/car.svg new file mode 100644 index 0000000..b71b817 --- /dev/null +++ b/.dev/browser/public/assets/tabler/car.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/cloud.svg b/.dev/browser/public/assets/tabler/cloud.svg new file mode 100644 index 0000000..4c66283 --- /dev/null +++ b/.dev/browser/public/assets/tabler/cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/code.svg b/.dev/browser/public/assets/tabler/code.svg new file mode 100644 index 0000000..97e7d07 --- /dev/null +++ b/.dev/browser/public/assets/tabler/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/coffee.svg b/.dev/browser/public/assets/tabler/coffee.svg new file mode 100644 index 0000000..b8e0236 --- /dev/null +++ b/.dev/browser/public/assets/tabler/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/compass.svg b/.dev/browser/public/assets/tabler/compass.svg new file mode 100644 index 0000000..086ca94 --- /dev/null +++ b/.dev/browser/public/assets/tabler/compass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/copy.svg b/.dev/browser/public/assets/tabler/copy.svg new file mode 100644 index 0000000..cf82fd6 --- /dev/null +++ b/.dev/browser/public/assets/tabler/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/cpu.svg b/.dev/browser/public/assets/tabler/cpu.svg new file mode 100644 index 0000000..3c0043b --- /dev/null +++ b/.dev/browser/public/assets/tabler/cpu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/database.svg b/.dev/browser/public/assets/tabler/database.svg new file mode 100644 index 0000000..072c7ba --- /dev/null +++ b/.dev/browser/public/assets/tabler/database.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/download.svg b/.dev/browser/public/assets/tabler/download.svg new file mode 100644 index 0000000..9fb8dbc --- /dev/null +++ b/.dev/browser/public/assets/tabler/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/edit.svg b/.dev/browser/public/assets/tabler/edit.svg new file mode 100644 index 0000000..14f71c8 --- /dev/null +++ b/.dev/browser/public/assets/tabler/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/eye.svg b/.dev/browser/public/assets/tabler/eye.svg new file mode 100644 index 0000000..86e50de --- /dev/null +++ b/.dev/browser/public/assets/tabler/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/file.svg b/.dev/browser/public/assets/tabler/file.svg new file mode 100644 index 0000000..acc16a0 --- /dev/null +++ b/.dev/browser/public/assets/tabler/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/flag.svg b/.dev/browser/public/assets/tabler/flag.svg new file mode 100644 index 0000000..d12b812 --- /dev/null +++ b/.dev/browser/public/assets/tabler/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/folder.svg b/.dev/browser/public/assets/tabler/folder.svg new file mode 100644 index 0000000..c6ca846 --- /dev/null +++ b/.dev/browser/public/assets/tabler/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/gift.svg b/.dev/browser/public/assets/tabler/gift.svg new file mode 100644 index 0000000..4bde760 --- /dev/null +++ b/.dev/browser/public/assets/tabler/gift.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/globe.svg b/.dev/browser/public/assets/tabler/globe.svg new file mode 100644 index 0000000..f01cad0 --- /dev/null +++ b/.dev/browser/public/assets/tabler/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/heart.svg b/.dev/browser/public/assets/tabler/heart.svg new file mode 100644 index 0000000..80ca6d0 --- /dev/null +++ b/.dev/browser/public/assets/tabler/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/home.svg b/.dev/browser/public/assets/tabler/home.svg new file mode 100644 index 0000000..3ce2580 --- /dev/null +++ b/.dev/browser/public/assets/tabler/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/key.svg b/.dev/browser/public/assets/tabler/key.svg new file mode 100644 index 0000000..8707fc3 --- /dev/null +++ b/.dev/browser/public/assets/tabler/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/link.svg b/.dev/browser/public/assets/tabler/link.svg new file mode 100644 index 0000000..ca5d9df --- /dev/null +++ b/.dev/browser/public/assets/tabler/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/lock.svg b/.dev/browser/public/assets/tabler/lock.svg new file mode 100644 index 0000000..29c3ff4 --- /dev/null +++ b/.dev/browser/public/assets/tabler/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/mail.svg b/.dev/browser/public/assets/tabler/mail.svg new file mode 100644 index 0000000..e7e1668 --- /dev/null +++ b/.dev/browser/public/assets/tabler/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/map-pin.svg b/.dev/browser/public/assets/tabler/map-pin.svg new file mode 100644 index 0000000..e974d6b --- /dev/null +++ b/.dev/browser/public/assets/tabler/map-pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/moon.svg b/.dev/browser/public/assets/tabler/moon.svg new file mode 100644 index 0000000..6aee6d6 --- /dev/null +++ b/.dev/browser/public/assets/tabler/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/music.svg b/.dev/browser/public/assets/tabler/music.svg new file mode 100644 index 0000000..b97d124 --- /dev/null +++ b/.dev/browser/public/assets/tabler/music.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/phone.svg b/.dev/browser/public/assets/tabler/phone.svg new file mode 100644 index 0000000..fa3208f --- /dev/null +++ b/.dev/browser/public/assets/tabler/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/photo.svg b/.dev/browser/public/assets/tabler/photo.svg new file mode 100644 index 0000000..3ad5c0c --- /dev/null +++ b/.dev/browser/public/assets/tabler/photo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/rocket.svg b/.dev/browser/public/assets/tabler/rocket.svg new file mode 100644 index 0000000..5191c09 --- /dev/null +++ b/.dev/browser/public/assets/tabler/rocket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/search.svg b/.dev/browser/public/assets/tabler/search.svg new file mode 100644 index 0000000..9bceb72 --- /dev/null +++ b/.dev/browser/public/assets/tabler/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/server.svg b/.dev/browser/public/assets/tabler/server.svg new file mode 100644 index 0000000..73c05f7 --- /dev/null +++ b/.dev/browser/public/assets/tabler/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/settings.svg b/.dev/browser/public/assets/tabler/settings.svg new file mode 100644 index 0000000..9e1e270 --- /dev/null +++ b/.dev/browser/public/assets/tabler/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/shield.svg b/.dev/browser/public/assets/tabler/shield.svg new file mode 100644 index 0000000..130fe5b --- /dev/null +++ b/.dev/browser/public/assets/tabler/shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/shopping-cart.svg b/.dev/browser/public/assets/tabler/shopping-cart.svg new file mode 100644 index 0000000..391d8fe --- /dev/null +++ b/.dev/browser/public/assets/tabler/shopping-cart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/star.svg b/.dev/browser/public/assets/tabler/star.svg new file mode 100644 index 0000000..f15280a --- /dev/null +++ b/.dev/browser/public/assets/tabler/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/sun.svg b/.dev/browser/public/assets/tabler/sun.svg new file mode 100644 index 0000000..d038bb3 --- /dev/null +++ b/.dev/browser/public/assets/tabler/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/tag.svg b/.dev/browser/public/assets/tabler/tag.svg new file mode 100644 index 0000000..5efd4c1 --- /dev/null +++ b/.dev/browser/public/assets/tabler/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/terminal.svg b/.dev/browser/public/assets/tabler/terminal.svg new file mode 100644 index 0000000..d157a11 --- /dev/null +++ b/.dev/browser/public/assets/tabler/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/thumb-up.svg b/.dev/browser/public/assets/tabler/thumb-up.svg new file mode 100644 index 0000000..e37731a --- /dev/null +++ b/.dev/browser/public/assets/tabler/thumb-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/trash.svg b/.dev/browser/public/assets/tabler/trash.svg new file mode 100644 index 0000000..decbda2 --- /dev/null +++ b/.dev/browser/public/assets/tabler/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/trending-up.svg b/.dev/browser/public/assets/tabler/trending-up.svg new file mode 100644 index 0000000..cd87497 --- /dev/null +++ b/.dev/browser/public/assets/tabler/trending-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/upload.svg b/.dev/browser/public/assets/tabler/upload.svg new file mode 100644 index 0000000..021124a --- /dev/null +++ b/.dev/browser/public/assets/tabler/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/user.svg b/.dev/browser/public/assets/tabler/user.svg new file mode 100644 index 0000000..2d1b790 --- /dev/null +++ b/.dev/browser/public/assets/tabler/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/video.svg b/.dev/browser/public/assets/tabler/video.svg new file mode 100644 index 0000000..28825d5 --- /dev/null +++ b/.dev/browser/public/assets/tabler/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/wallet.svg b/.dev/browser/public/assets/tabler/wallet.svg new file mode 100644 index 0000000..6c39dd8 --- /dev/null +++ b/.dev/browser/public/assets/tabler/wallet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/wifi.svg b/.dev/browser/public/assets/tabler/wifi.svg new file mode 100644 index 0000000..6e80c59 --- /dev/null +++ b/.dev/browser/public/assets/tabler/wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/tabler/world.svg b/.dev/browser/public/assets/tabler/world.svg new file mode 100644 index 0000000..bc353ff --- /dev/null +++ b/.dev/browser/public/assets/tabler/world.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/alert-circle.svg b/.dev/browser/public/assets/ui/alert-circle.svg new file mode 100644 index 0000000..9869e04 --- /dev/null +++ b/.dev/browser/public/assets/ui/alert-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/arrows-up-down.svg b/.dev/browser/public/assets/ui/arrows-up-down.svg new file mode 100644 index 0000000..dfad3ee --- /dev/null +++ b/.dev/browser/public/assets/ui/arrows-up-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/bell-filled.svg b/.dev/browser/public/assets/ui/bell-filled.svg new file mode 100644 index 0000000..fb18d60 --- /dev/null +++ b/.dev/browser/public/assets/ui/bell-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/category.svg b/.dev/browser/public/assets/ui/category.svg new file mode 100644 index 0000000..203bce4 --- /dev/null +++ b/.dev/browser/public/assets/ui/category.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/check.svg b/.dev/browser/public/assets/ui/check.svg new file mode 100644 index 0000000..e4ea94f --- /dev/null +++ b/.dev/browser/public/assets/ui/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevron-down.svg b/.dev/browser/public/assets/ui/chevron-down.svg new file mode 100644 index 0000000..69de902 --- /dev/null +++ b/.dev/browser/public/assets/ui/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevron-left.svg b/.dev/browser/public/assets/ui/chevron-left.svg new file mode 100644 index 0000000..f64bc2a --- /dev/null +++ b/.dev/browser/public/assets/ui/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevron-right.svg b/.dev/browser/public/assets/ui/chevron-right.svg new file mode 100644 index 0000000..1bb2133 --- /dev/null +++ b/.dev/browser/public/assets/ui/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevron-up.svg b/.dev/browser/public/assets/ui/chevron-up.svg new file mode 100644 index 0000000..d2e438d --- /dev/null +++ b/.dev/browser/public/assets/ui/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevrons-left.svg b/.dev/browser/public/assets/ui/chevrons-left.svg new file mode 100644 index 0000000..fa4e99d --- /dev/null +++ b/.dev/browser/public/assets/ui/chevrons-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/chevrons-right.svg b/.dev/browser/public/assets/ui/chevrons-right.svg new file mode 100644 index 0000000..4ab56d5 --- /dev/null +++ b/.dev/browser/public/assets/ui/chevrons-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/clock.svg b/.dev/browser/public/assets/ui/clock.svg new file mode 100644 index 0000000..ec45f33 --- /dev/null +++ b/.dev/browser/public/assets/ui/clock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/close.svg b/.dev/browser/public/assets/ui/close.svg new file mode 100644 index 0000000..77d6137 --- /dev/null +++ b/.dev/browser/public/assets/ui/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/command.svg b/.dev/browser/public/assets/ui/command.svg new file mode 100644 index 0000000..f42869f --- /dev/null +++ b/.dev/browser/public/assets/ui/command.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/copy.svg b/.dev/browser/public/assets/ui/copy.svg new file mode 100644 index 0000000..9c1ea08 --- /dev/null +++ b/.dev/browser/public/assets/ui/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/cube.svg b/.dev/browser/public/assets/ui/cube.svg new file mode 100644 index 0000000..0035540 --- /dev/null +++ b/.dev/browser/public/assets/ui/cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/dots-vertical.svg b/.dev/browser/public/assets/ui/dots-vertical.svg new file mode 100644 index 0000000..3232e77 --- /dev/null +++ b/.dev/browser/public/assets/ui/dots-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/download.svg b/.dev/browser/public/assets/ui/download.svg new file mode 100644 index 0000000..db425e9 --- /dev/null +++ b/.dev/browser/public/assets/ui/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/edit.svg b/.dev/browser/public/assets/ui/edit.svg new file mode 100644 index 0000000..376e372 --- /dev/null +++ b/.dev/browser/public/assets/ui/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/eye.svg b/.dev/browser/public/assets/ui/eye.svg new file mode 100644 index 0000000..c23f081 --- /dev/null +++ b/.dev/browser/public/assets/ui/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/filter.svg b/.dev/browser/public/assets/ui/filter.svg new file mode 100644 index 0000000..bde5b2a --- /dev/null +++ b/.dev/browser/public/assets/ui/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/folder-filled.svg b/.dev/browser/public/assets/ui/folder-filled.svg new file mode 100644 index 0000000..77d54b4 --- /dev/null +++ b/.dev/browser/public/assets/ui/folder-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/folder-open.svg b/.dev/browser/public/assets/ui/folder-open.svg new file mode 100644 index 0000000..203bce4 --- /dev/null +++ b/.dev/browser/public/assets/ui/folder-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/folder.svg b/.dev/browser/public/assets/ui/folder.svg new file mode 100644 index 0000000..e127267 --- /dev/null +++ b/.dev/browser/public/assets/ui/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/grid.svg b/.dev/browser/public/assets/ui/grid.svg new file mode 100644 index 0000000..299844f --- /dev/null +++ b/.dev/browser/public/assets/ui/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/heart-filled.svg b/.dev/browser/public/assets/ui/heart-filled.svg new file mode 100644 index 0000000..2ea6a74 --- /dev/null +++ b/.dev/browser/public/assets/ui/heart-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/heart.svg b/.dev/browser/public/assets/ui/heart.svg new file mode 100644 index 0000000..588fac0 --- /dev/null +++ b/.dev/browser/public/assets/ui/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/info.svg b/.dev/browser/public/assets/ui/info.svg new file mode 100644 index 0000000..4daf8e6 --- /dev/null +++ b/.dev/browser/public/assets/ui/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/layers.svg b/.dev/browser/public/assets/ui/layers.svg new file mode 100644 index 0000000..2884b91 --- /dev/null +++ b/.dev/browser/public/assets/ui/layers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/link.svg b/.dev/browser/public/assets/ui/link.svg new file mode 100644 index 0000000..1ff8797 --- /dev/null +++ b/.dev/browser/public/assets/ui/link.svg @@ -0,0 +1 @@ + diff --git a/.dev/browser/public/assets/ui/list.svg b/.dev/browser/public/assets/ui/list.svg new file mode 100644 index 0000000..1129943 --- /dev/null +++ b/.dev/browser/public/assets/ui/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/loader.svg b/.dev/browser/public/assets/ui/loader.svg new file mode 100644 index 0000000..c8af371 --- /dev/null +++ b/.dev/browser/public/assets/ui/loader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/moon.svg b/.dev/browser/public/assets/ui/moon.svg new file mode 100644 index 0000000..361bc20 --- /dev/null +++ b/.dev/browser/public/assets/ui/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/package.svg b/.dev/browser/public/assets/ui/package.svg new file mode 100644 index 0000000..0035540 --- /dev/null +++ b/.dev/browser/public/assets/ui/package.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/palette.svg b/.dev/browser/public/assets/ui/palette.svg new file mode 100644 index 0000000..b0970fb --- /dev/null +++ b/.dev/browser/public/assets/ui/palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/plus.svg b/.dev/browser/public/assets/ui/plus.svg new file mode 100644 index 0000000..62a2cac --- /dev/null +++ b/.dev/browser/public/assets/ui/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/refresh.svg b/.dev/browser/public/assets/ui/refresh.svg new file mode 100644 index 0000000..c8af371 --- /dev/null +++ b/.dev/browser/public/assets/ui/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/resize.svg b/.dev/browser/public/assets/ui/resize.svg new file mode 100644 index 0000000..4e8f7e0 --- /dev/null +++ b/.dev/browser/public/assets/ui/resize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/search.svg b/.dev/browser/public/assets/ui/search.svg new file mode 100644 index 0000000..5fd5a01 --- /dev/null +++ b/.dev/browser/public/assets/ui/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/settings.svg b/.dev/browser/public/assets/ui/settings.svg new file mode 100644 index 0000000..6e46ecf --- /dev/null +++ b/.dev/browser/public/assets/ui/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sliders.svg b/.dev/browser/public/assets/ui/sliders.svg new file mode 100644 index 0000000..2712538 --- /dev/null +++ b/.dev/browser/public/assets/ui/sliders.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sort-asc.svg b/.dev/browser/public/assets/ui/sort-asc.svg new file mode 100644 index 0000000..ee759ac --- /dev/null +++ b/.dev/browser/public/assets/ui/sort-asc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sort-desc.svg b/.dev/browser/public/assets/ui/sort-desc.svg new file mode 100644 index 0000000..eab1d3d --- /dev/null +++ b/.dev/browser/public/assets/ui/sort-desc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sort.svg b/.dev/browser/public/assets/ui/sort.svg new file mode 100644 index 0000000..ee759ac --- /dev/null +++ b/.dev/browser/public/assets/ui/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sparkles.svg b/.dev/browser/public/assets/ui/sparkles.svg new file mode 100644 index 0000000..9a0d58c --- /dev/null +++ b/.dev/browser/public/assets/ui/sparkles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/spinner.svg b/.dev/browser/public/assets/ui/spinner.svg new file mode 100644 index 0000000..b7506de --- /dev/null +++ b/.dev/browser/public/assets/ui/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/star-filled.svg b/.dev/browser/public/assets/ui/star-filled.svg new file mode 100644 index 0000000..c2f7b98 --- /dev/null +++ b/.dev/browser/public/assets/ui/star-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/star.svg b/.dev/browser/public/assets/ui/star.svg new file mode 100644 index 0000000..2626427 --- /dev/null +++ b/.dev/browser/public/assets/ui/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/stats.svg b/.dev/browser/public/assets/ui/stats.svg new file mode 100644 index 0000000..416dc1b --- /dev/null +++ b/.dev/browser/public/assets/ui/stats.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/sun.svg b/.dev/browser/public/assets/ui/sun.svg new file mode 100644 index 0000000..43b8d5f --- /dev/null +++ b/.dev/browser/public/assets/ui/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/text.svg b/.dev/browser/public/assets/ui/text.svg new file mode 100644 index 0000000..575d7f1 --- /dev/null +++ b/.dev/browser/public/assets/ui/text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/trash.svg b/.dev/browser/public/assets/ui/trash.svg new file mode 100644 index 0000000..4e7b0fe --- /dev/null +++ b/.dev/browser/public/assets/ui/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/assets/ui/zap.svg b/.dev/browser/public/assets/ui/zap.svg new file mode 100644 index 0000000..5851609 --- /dev/null +++ b/.dev/browser/public/assets/ui/zap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.dev/browser/public/data/app-config.json b/.dev/browser/public/data/app-config.json new file mode 100644 index 0000000..98def74 --- /dev/null +++ b/.dev/browser/public/data/app-config.json @@ -0,0 +1,154 @@ +{ + "meta": { + "generated": "2026-07-21", + "author": "Simtabi LLC", + "sets": 70, + "license": "Proprietary", + "url": "https://simtabi.com" + }, + "seo": { + "title": "Ichava Browser — every icon your Laravel app needs", + "description": "Any SVG, icons and illustrations, from open-source sets. Search, restyle, animate and copy — visual browser, REST API and Blade components for Laravel." + }, + "features": { + "devtools": true, + "collections": true, + "history": true, + "motion": true, + "treatments": true, + "multiSelect": true, + "presence": true, + "demo": true + }, + "user": { + "mode": "guest", + "name": "Amina K.", + "email": "amina@simtabi.com", + "initials": "AK", + "plan": "TEAM", + "workspaces": [ + { + "id": "simtabi", + "name": "Simtabi Team", + "plan": "TEAM", + "active": true + }, + { + "id": "personal", + "name": "Personal", + "plan": "FREE", + "active": false + } + ] + }, + "ui": { + "defaultTheme": "light", + "scale": "m", + "density": "compact", + "accentOptions": [ + "#0e7c66", + "#2f54d6", + "#c2410c", + "#7c3aed", + "#be185d" + ], + "perPageOptions": [ + 30, + 60, + 120 + ] + }, + "storage": { + "frontendDriver": "local", + "laravelDriver": "session" + }, + "boot": { + "variant": "bar", + "background": "gradient", + "brand": { + "tagline": "Every icon your Laravel app needs" + }, + "showStats": true, + "tips": { + "enabled": true, + "items": [ + "Press ⌘K anywhere to open the command palette.", + "Icons inherit currentColor — theme them with one CSS variable.", + "Star an icon to add it to your Favorites." + ] + }, + "stats": [ + { + "key": "icons", + "label": "Icons", + "to": 8686 + }, + { + "key": "packages", + "label": "Packages", + "to": 3 + }, + { + "key": "categories", + "label": "Categories", + "to": 84 + } + ] + }, + "team": [ + { + "initials": "JD", + "name": "Jonas D.", + "online": true, + "status": "online" + }, + { + "initials": "RK", + "name": "Rita K.", + "online": true, + "status": "online" + }, + { + "initials": "MO", + "name": "Moses O.", + "online": false, + "status": "away" + } + ], + "defaults": { + "collections": [ + { + "id": "c-nav", + "name": "Nav icons", + "icons": [ + 1, + 3, + 2 + ] + }, + { + "id": "c-dashboard", + "name": "Dashboard set", + "icons": [ + 38, + 2, + 10, + 5, + 24 + ], + "shared": true, + "role": "editor", + "sharedWith": [ + { + "initials": "JD", + "name": "Jonas D." + }, + { + "initials": "RK", + "name": "Rita K." + } + ] + } + ] + } +} diff --git a/.dev/browser/public/data/icons.json b/.dev/browser/public/data/icons.json new file mode 100644 index 0000000..c15a748 --- /dev/null +++ b/.dev/browser/public/data/icons.json @@ -0,0 +1,2322 @@ +{ + "meta": { + "total_ecosystem": 18783, + "generated": "2026-10-26" + }, + "packages": [ + { + "id": "ichava/icon-sets-flag", + "label": "icon-sets-flag", + "description": "271 country flag SVGs sourced from lipis/flag-icons, available in 4x3 and 1x1 aspect ratios. Replaces the flag half of the deprecated simtabi/laflamoji.", + "count": 542, + "installed": true, + "loaded": true + }, + { + "id": "ichava/icon-sets-openmoji", + "label": "icon-sets-openmoji", + "description": "OpenMoji emoji in two sets (colour and black outline), Unicode emoji 16.0, grouped by Unicode CLDR category. Moved verbatim from ichava/icon-sets-twemoji.", + "count": 8568, + "installed": true, + "loaded": true + }, + { + "id": "ichava/icon-sets-tabler", + "label": "icon-sets-tabler", + "description": "Over 5,200 pixel-perfect icons for web apps. Free and open source icons designed with attention to detail to make your design stand out.", + "count": 6238, + "installed": true, + "loaded": true + }, + { + "id": "ichava/icon-sets-twemoji", + "label": "icon-sets-twemoji", + "description": "Twemoji, Twitter/X's open-source emoji set, as SVG. Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups. OpenMoji is in ichava/icon-sets-openmoji.", + "count": 3435, + "installed": true, + "loaded": true + } + ], + "icons": [ + { + "id": 1, + "package": "ichava/icon-sets-openmoji", + "name": "1st-place-medal", + "category": "openmoji-black", + "sub": "activities", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/activities/1st-place-medal.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/activities/1st-place-medal", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 2, + "package": "ichava/icon-sets-openmoji", + "name": "coconut", + "category": "openmoji-black", + "sub": "food-drink", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/food-drink/coconut.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/food-drink/coconut", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 3, + "package": "ichava/icon-sets-openmoji", + "name": "deaf-woman-medium-dark-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/deaf-woman-medium-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/deaf-woman-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 4, + "package": "ichava/icon-sets-openmoji", + "name": "ferris-wheel", + "category": "openmoji-black", + "sub": "travel-places", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/travel-places/ferris-wheel.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/travel-places/ferris-wheel", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 5, + "package": "ichava/icon-sets-openmoji", + "name": "flag-burkina-faso", + "category": "openmoji-black", + "sub": "flags", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/flags/flag-burkina-faso.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/flags/flag-burkina-faso", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 6, + "package": "ichava/icon-sets-openmoji", + "name": "kiss-mark", + "category": "openmoji-black", + "sub": "smileys-emotion", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/smileys-emotion/kiss-mark.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/smileys-emotion/kiss-mark", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 7, + "package": "ichava/icon-sets-openmoji", + "name": "kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/kiss-woman-woman-dark-skin-tone-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 8, + "package": "ichava/icon-sets-openmoji", + "name": "man-in-steamy-room-medium-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/man-in-steamy-room-medium-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/man-in-steamy-room-medium-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 9, + "package": "ichava/icon-sets-openmoji", + "name": "mechanic-medium-dark-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/mechanic-medium-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/mechanic-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 10, + "package": "ichava/icon-sets-openmoji", + "name": "person-golfing-medium-light-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/person-golfing-medium-light-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/person-golfing-medium-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 11, + "package": "ichava/icon-sets-openmoji", + "name": "police-officer", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/police-officer.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/police-officer", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 12, + "package": "ichava/icon-sets-openmoji", + "name": "sari", + "category": "openmoji-black", + "sub": "objects", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/objects/sari.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/objects/sari", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 13, + "package": "ichava/icon-sets-openmoji", + "name": "woman-dancing-medium-light-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/woman-dancing-medium-light-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/woman-dancing-medium-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 14, + "package": "ichava/icon-sets-openmoji", + "name": "woman-raising-hand-medium-light-skin-tone", + "category": "openmoji-black", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-black/people-body/woman-raising-hand-medium-light-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-black/people-body/woman-raising-hand-medium-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 15, + "package": "ichava/icon-sets-openmoji", + "name": "cigarette", + "category": "openmoji-color", + "sub": "objects", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/objects/cigarette.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/objects/cigarette", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 16, + "package": "ichava/icon-sets-openmoji", + "name": "construction-worker-medium-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/construction-worker-medium-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/construction-worker-medium-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 17, + "package": "ichava/icon-sets-openmoji", + "name": "flag-nigeria", + "category": "openmoji-color", + "sub": "flags", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/flags/flag-nigeria.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/flags/flag-nigeria", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 18, + "package": "ichava/icon-sets-openmoji", + "name": "guide-dog", + "category": "openmoji-color", + "sub": "animals-nature", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/animals-nature/guide-dog.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/animals-nature/guide-dog", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 19, + "package": "ichava/icon-sets-openmoji", + "name": "handshake-light-skin-tone-medium-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/handshake-light-skin-tone-medium-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/handshake-light-skin-tone-medium-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 20, + "package": "ichava/icon-sets-openmoji", + "name": "japanese-discount-button", + "category": "openmoji-color", + "sub": "symbols", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/symbols/japanese-discount-button.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/symbols/japanese-discount-button", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 21, + "package": "ichava/icon-sets-openmoji", + "name": "man-dark-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/man-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/man-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 22, + "package": "ichava/icon-sets-openmoji", + "name": "man-rowing-boat", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/man-rowing-boat.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/man-rowing-boat", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 23, + "package": "ichava/icon-sets-openmoji", + "name": "palm-down-hand-light-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/palm-down-hand-light-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/palm-down-hand-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 24, + "package": "ichava/icon-sets-openmoji", + "name": "person-raising-hand-medium-dark-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/person-raising-hand-medium-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/person-raising-hand-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 25, + "package": "ichava/icon-sets-openmoji", + "name": "technologist-dark-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/technologist-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/technologist-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 26, + "package": "ichava/icon-sets-openmoji", + "name": "woman-in-motorized-wheelchair-medium-dark-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/woman-in-motorized-wheelchair-medium-dark-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/woman-in-motorized-wheelchair-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 27, + "package": "ichava/icon-sets-openmoji", + "name": "woman-with-white-cane-facing-right-light-skin-tone", + "category": "openmoji-color", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-openmoji/openmoji-color/people-body/woman-with-white-cane-facing-right-light-skin-tone.svg", + "viewBox": "0 0 72 72", + "path": "ichava/icon-sets-openmoji::openmoji-color/people-body/woman-with-white-cane-facing-right-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 28, + "package": "ichava/icon-sets-twemoji", + "name": "1st-place-medal", + "category": "twemoji", + "sub": "activities", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/activities/1st-place-medal.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/activities/1st-place-medal", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 29, + "package": "ichava/icon-sets-twemoji", + "name": "croissant", + "category": "twemoji", + "sub": "food-drink", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/food-drink/croissant.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/food-drink/croissant", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 30, + "package": "ichava/icon-sets-twemoji", + "name": "detective-medium-dark-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/detective-medium-dark-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/detective-medium-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 31, + "package": "ichava/icon-sets-twemoji", + "name": "flag-burundi", + "category": "twemoji", + "sub": "flags", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/flags/flag-burundi.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/flags/flag-burundi", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 32, + "package": "ichava/icon-sets-twemoji", + "name": "heavy-dollar-sign", + "category": "twemoji", + "sub": "symbols", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/symbols/heavy-dollar-sign.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/symbols/heavy-dollar-sign", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 33, + "package": "ichava/icon-sets-twemoji", + "name": "kiss-woman-woman-medium-dark-skin-tone-light-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/kiss-woman-woman-medium-dark-skin-tone-light-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/kiss-woman-woman-medium-dark-skin-tone-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 34, + "package": "ichava/icon-sets-twemoji", + "name": "man-kneeling-light-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/man-kneeling-light-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/man-kneeling-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 35, + "package": "ichava/icon-sets-twemoji", + "name": "merman", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/merman.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/merman", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 36, + "package": "ichava/icon-sets-twemoji", + "name": "person-juggling-medium-light-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/person-juggling-medium-light-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/person-juggling-medium-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 37, + "package": "ichava/icon-sets-twemoji", + "name": "scientist-medium-light-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/scientist-medium-light-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/scientist-medium-light-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 38, + "package": "ichava/icon-sets-twemoji", + "name": "shopping-cart", + "category": "twemoji", + "sub": "objects", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/objects/shopping-cart.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/objects/shopping-cart", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 39, + "package": "ichava/icon-sets-twemoji", + "name": "woman-getting-haircut", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/woman-getting-haircut.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/woman-getting-haircut", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 40, + "package": "ichava/icon-sets-twemoji", + "name": "woman-wearing-turban-dark-skin-tone", + "category": "twemoji", + "sub": "people-body", + "variant": "default", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-twemoji/twemoji/people-body/woman-wearing-turban-dark-skin-tone.svg", + "viewBox": "0 0 36 36", + "path": "ichava/icon-sets-twemoji::twemoji/people-body/woman-wearing-turban-dark-skin-tone", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 41, + "package": "ichava/icon-sets-flag", + "name": "ad", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/ad.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/ad", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 42, + "package": "ichava/icon-sets-flag", + "name": "ad", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/ad.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/ad", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 43, + "package": "ichava/icon-sets-flag", + "name": "ax", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/ax.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/ax", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 44, + "package": "ichava/icon-sets-flag", + "name": "ax", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/ax.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/ax", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 45, + "package": "ichava/icon-sets-flag", + "name": "bs", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/bs.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/bs", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 46, + "package": "ichava/icon-sets-flag", + "name": "bs", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/bs.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/bs", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 47, + "package": "ichava/icon-sets-flag", + "name": "cn", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/cn.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/cn", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 48, + "package": "ichava/icon-sets-flag", + "name": "cn", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/cn.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/cn", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 49, + "package": "ichava/icon-sets-flag", + "name": "dz", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/dz.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/dz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 50, + "package": "ichava/icon-sets-flag", + "name": "dz", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/dz.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/dz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 51, + "package": "ichava/icon-sets-flag", + "name": "fo", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/fo.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/fo", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 52, + "package": "ichava/icon-sets-flag", + "name": "fo", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/fo.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/fo", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 53, + "package": "ichava/icon-sets-flag", + "name": "gb", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/gb.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/gb", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 54, + "package": "ichava/icon-sets-flag", + "name": "gb", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/gb.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/gb", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 55, + "package": "ichava/icon-sets-flag", + "name": "gq", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/gq.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/gq", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 56, + "package": "ichava/icon-sets-flag", + "name": "gq", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/gq.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/gq", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 57, + "package": "ichava/icon-sets-flag", + "name": "il", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/il.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/il", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 58, + "package": "ichava/icon-sets-flag", + "name": "il", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/il.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/il", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 59, + "package": "ichava/icon-sets-flag", + "name": "ke", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/ke.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/ke", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 60, + "package": "ichava/icon-sets-flag", + "name": "ke", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/ke.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/ke", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 61, + "package": "ichava/icon-sets-flag", + "name": "kp", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/kp.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/kp", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 62, + "package": "ichava/icon-sets-flag", + "name": "kp", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/kp.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/kp", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 63, + "package": "ichava/icon-sets-flag", + "name": "mc", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/mc.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/mc", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 64, + "package": "ichava/icon-sets-flag", + "name": "mc", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/mc.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/mc", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 65, + "package": "ichava/icon-sets-flag", + "name": "mu", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/mu.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/mu", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 66, + "package": "ichava/icon-sets-flag", + "name": "mu", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/mu.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/mu", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 67, + "package": "ichava/icon-sets-flag", + "name": "nz", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/nz.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/nz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 68, + "package": "ichava/icon-sets-flag", + "name": "nz", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/nz.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/nz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 69, + "package": "ichava/icon-sets-flag", + "name": "qa", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/qa.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/qa", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 70, + "package": "ichava/icon-sets-flag", + "name": "qa", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/qa.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/qa", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 71, + "package": "ichava/icon-sets-flag", + "name": "si", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/si.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/si", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 72, + "package": "ichava/icon-sets-flag", + "name": "si", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/si.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/si", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 73, + "package": "ichava/icon-sets-flag", + "name": "tg", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/tg.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/tg", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 74, + "package": "ichava/icon-sets-flag", + "name": "tg", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/tg.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/tg", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 75, + "package": "ichava/icon-sets-flag", + "name": "tz", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/tz.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/tz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 76, + "package": "ichava/icon-sets-flag", + "name": "tz", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/tz.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/tz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 77, + "package": "ichava/icon-sets-flag", + "name": "us", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/us.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/us", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 78, + "package": "ichava/icon-sets-flag", + "name": "us", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/us.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/us", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 79, + "package": "ichava/icon-sets-flag", + "name": "uz", + "category": "general", + "sub": null, + "variant": "1x1", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/1x1/uz.svg", + "viewBox": "0 0 512 512", + "path": "ichava/icon-sets-flag::1x1/uz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 80, + "package": "ichava/icon-sets-flag", + "name": "uz", + "category": "general", + "sub": null, + "variant": "4x3", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-flag/4x3/uz.svg", + "viewBox": "0 0 640 480", + "path": "ichava/icon-sets-flag::4x3/uz", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": true + }, + { + "id": 81, + "package": "ichava/icon-sets-tabler", + "name": "accessible", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/accessible.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/accessible", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 82, + "package": "ichava/icon-sets-tabler", + "name": "arrow-ramp-right-3", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/arrow-ramp-right-3.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/arrow-ramp-right-3", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 83, + "package": "ichava/icon-sets-tabler", + "name": "bolt", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/bolt.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/bolt", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 84, + "package": "ichava/icon-sets-tabler", + "name": "bolt", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/bolt.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/bolt", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 85, + "package": "ichava/icon-sets-tabler", + "name": "bottle", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/bottle.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/bottle", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 86, + "package": "ichava/icon-sets-tabler", + "name": "brand-stackshare", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/brand-stackshare.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/brand-stackshare", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 87, + "package": "ichava/icon-sets-tabler", + "name": "camera", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/camera.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/camera", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 88, + "package": "ichava/icon-sets-tabler", + "name": "camera", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/camera.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/camera", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 89, + "package": "ichava/icon-sets-tabler", + "name": "chair-director", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/chair-director.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/chair-director", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 90, + "package": "ichava/icon-sets-tabler", + "name": "circle-number-9", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/circle-number-9.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/circle-number-9", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 91, + "package": "ichava/icon-sets-tabler", + "name": "cloud", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/cloud.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/cloud", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 92, + "package": "ichava/icon-sets-tabler", + "name": "cloud", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/cloud.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/cloud", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 93, + "package": "ichava/icon-sets-tabler", + "name": "cloud-cog", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/cloud-cog.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/cloud-cog", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 94, + "package": "ichava/icon-sets-tabler", + "name": "code", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/code.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/code", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 95, + "package": "ichava/icon-sets-tabler", + "name": "database", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/database.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/database", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 96, + "package": "ichava/icon-sets-tabler", + "name": "database", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/database.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/database", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 97, + "package": "ichava/icon-sets-tabler", + "name": "device-imac-off", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/device-imac-off.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/device-imac-off", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 98, + "package": "ichava/icon-sets-tabler", + "name": "eye-pin", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/eye-pin.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/eye-pin", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 99, + "package": "ichava/icon-sets-tabler", + "name": "git-branch-check", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/git-branch-check.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/git-branch-check", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 100, + "package": "ichava/icon-sets-tabler", + "name": "heart", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/heart.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/heart", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 101, + "package": "ichava/icon-sets-tabler", + "name": "heart", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/heart.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/heart", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 102, + "package": "ichava/icon-sets-tabler", + "name": "home", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/home.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/home", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 103, + "package": "ichava/icon-sets-tabler", + "name": "home", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/home.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/home", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 104, + "package": "ichava/icon-sets-tabler", + "name": "join-straight", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/join-straight.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/join-straight", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 105, + "package": "ichava/icon-sets-tabler", + "name": "layout-dashboard", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/layout-dashboard.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/layout-dashboard", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 106, + "package": "ichava/icon-sets-tabler", + "name": "map-pin-off", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/map-pin-off.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/map-pin-off", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 107, + "package": "ichava/icon-sets-tabler", + "name": "navigation-plus", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/navigation-plus.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/navigation-plus", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 108, + "package": "ichava/icon-sets-tabler", + "name": "physiotherapist", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/physiotherapist.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/physiotherapist", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 109, + "package": "ichava/icon-sets-tabler", + "name": "rocket", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/rocket.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/rocket", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 110, + "package": "ichava/icon-sets-tabler", + "name": "run-sprint", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/run-sprint.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/run-sprint", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 111, + "package": "ichava/icon-sets-tabler", + "name": "search", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/search.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/search", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 112, + "package": "ichava/icon-sets-tabler", + "name": "search", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/search.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/search", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 113, + "package": "ichava/icon-sets-tabler", + "name": "settings", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/settings.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/settings", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 114, + "package": "ichava/icon-sets-tabler", + "name": "settings", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/settings.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/settings", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 115, + "package": "ichava/icon-sets-tabler", + "name": "shield", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/shield.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/shield", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 116, + "package": "ichava/icon-sets-tabler", + "name": "shield", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/shield.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/shield", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 117, + "package": "ichava/icon-sets-tabler", + "name": "square-letter-y", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/square-letter-y.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/square-letter-y", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 118, + "package": "ichava/icon-sets-tabler", + "name": "swipe-right", + "category": "general", + "sub": null, + "variant": "filled", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/filled/swipe-right.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::filled/swipe-right", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 119, + "package": "ichava/icon-sets-tabler", + "name": "terminal", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/terminal.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/terminal", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + }, + { + "id": 120, + "package": "ichava/icon-sets-tabler", + "name": "traffic-cone-off", + "category": "general", + "sub": null, + "variant": "outline", + "kind": "icon", + "svgContent": null, + "svgUrl": "assets/icon-sets-tabler/outline/traffic-cone-off.svg", + "viewBox": "0 0 24 24", + "path": "ichava/icon-sets-tabler::outline/traffic-cone-off", + "bladeClean": "", + "bladeGeneric": "", + "helper": "", + "tags": [], + "keywords": [], + "ownColor": false + } + ] +} diff --git a/.dev/browser/rewrite-lib-type-paths.mjs b/.dev/browser/rewrite-lib-type-paths.mjs new file mode 100644 index 0000000..2bda265 --- /dev/null +++ b/.dev/browser/rewrite-lib-type-paths.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +// Rewrites this repo's `@/` path alias to relative specifiers in the emitted +// declarations under dist/lib/types. +// +// tsc resolves `paths` for type checking but never rewrites them in its output, so +// `export { IchavaBrowser } from '@/IchavaBrowser'` would ship verbatim and resolve to +// nothing in a host. The JS side is resolved by Vite; this is the .d.ts side of the +// same fix (FINDINGS B2). Run after `tsc -p tsconfig.lib.json`. + +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const typesDir = join(root, 'dist/lib/types'); + +function walk(dir) { + return readdirSync(dir).flatMap((name) => { + const p = join(dir, name); + return statSync(p).isDirectory() ? walk(p) : p.endsWith('.d.ts') ? [p] : []; + }); +} + +const files = walk(typesDir); +if (files.length === 0) { + console.error(`rewrite-lib-type-paths: no declarations under ${relative(root, typesDir)}; run tsc -p tsconfig.lib.json first`); + process.exit(1); +} + +let rewritten = 0; +for (const file of files) { + const src = readFileSync(file, 'utf8'); + const out = src.replace(/(['"])@\/([^'"]+)\1/g, (_m, q, rest) => { + let rel = relative(dirname(file), join(typesDir, rest)).split(sep).join('/'); + if (!rel.startsWith('.')) rel = `./${rel}`; + rewritten++; + return `${q}${rel}${q}`; + }); + if (out !== src) writeFileSync(file, out); +} +console.log(`rewrite-lib-type-paths: ${files.length} declaration files, ${rewritten} alias specifiers rewritten`); diff --git a/.dev/browser/sample-svg-archetypes.mjs b/.dev/browser/sample-svg-archetypes.mjs new file mode 100644 index 0000000..060f155 --- /dev/null +++ b/.dev/browser/sample-svg-archetypes.mjs @@ -0,0 +1,79 @@ +/** + * Sample SVG archetypes for the W1 P1 rendering baseline. + * + * Samples by FEATURE rather than by pack: a pack is not an archetype. All 4,000 tabler + * icons are the same monochrome shape, while five metronic files carry + * `foreignObject`/`script`/`image` -- exactly what the sanitisation work in P4 targets. + * Three per feature per pack covers every structural variation present. + * + * Deterministic: directory entries are sorted before sampling, so re-running picks the + * same files and the baseline does not churn. + * + * Writes to `public/baseline/` plus a manifest the spec reads. + */ +import { readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { join, basename, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Derived, not written down. This was an absolute path into one machine's home +// directory, beside an `OUT` that assumed the pre-`packages/` tree and a PACKS +// list of directory names that the 2026-09-21 rebrand renamed -- three ways of +// being wrong, none of which anything reported, because the script is run by +// hand and is not wired to an npm script. +const APP = dirname(dirname(fileURLToPath(import.meta.url))); +const ROOT = dirname(APP); +const OUT = join(APP, 'public', 'baseline'); +const PACKS = ['icon-sets-tabler', 'icon-sets-bundled', 'icon-sets-metronic', 'icon-sets-flag']; + +const FEATURES = { + style_block: (s) => /]/i.test(s), + defs_ids: (s) => /]/i.test(s) && /\bid=/.test(s), + use_ref: (s) => /]/i.test(s), + gradient: (s) => /<(linear|radial)Gradient/i.test(s), + clip_or_mask: (s) => /<(clipPath|mask)[\s>]/i.test(s), + own_colour: (s) => /(fill|stroke)\s*=\s*"#|rgb\(|hsl\(/i.test(s), + current_color: (s) => /currentColor/i.test(s), + filter_fx: (s) => /]/i.test(s), + foreign: (s) => /<(foreignObject|script|image)[\s>]/i.test(s), +}; +const PER_FEATURE = 3; + +function walk(dir, out = [], cap = 6000) { + if (out.length >= cap) return out; + let es; try { es = readdirSync(dir); } catch { return out; } + for (const e of es.sort()) { // sorted: deterministic sampling + if (out.length >= cap) break; + const p = join(dir, e); + let st; try { st = statSync(p); } catch { continue; } + if (st.isDirectory()) walk(p, out, cap); + else if (p.endsWith('.svg')) out.push(p); + } + return out; +} + +rmSync(OUT, { recursive: true, force: true }); +mkdirSync(OUT, { recursive: true }); + +const manifest = []; +for (const pack of PACKS) { + const files = walk(join(ROOT, pack, 'resources/assets/svg')); + const taken = new Set(); + for (const [feat, test] of Object.entries(FEATURES)) { + let n = 0; + for (const f of files) { + if (n >= PER_FEATURE) break; + if (taken.has(f)) continue; + let s; try { s = readFileSync(f, 'utf8'); } catch { continue; } + if (!test(s)) continue; + taken.add(f); n++; + const name = `${pack}__${feat}__${basename(f)}`; + writeFileSync(join(OUT, name), s); + manifest.push({ pack, feature: feat, file: name, bytes: s.length }); + } + } +} +writeFileSync(join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2)); +console.log(` sampled ${manifest.length} svgs across ${PACKS.length} packs`); +const byFeat = {}; +for (const m of manifest) (byFeat[m.feature] ??= []).push(m.pack); +for (const [f, packs] of Object.entries(byFeat)) console.log(` ${f.padEnd(15)} ${packs.length} from ${[...new Set(packs)].join(', ')}`); diff --git a/.dev/browser/schema-types.d.mts b/.dev/browser/schema-types.d.mts new file mode 100644 index 0000000..d30beaf --- /dev/null +++ b/.dev/browser/schema-types.d.mts @@ -0,0 +1,11 @@ +export const SCHEMA_PATH: string; +export const SOURCE_PATH: string; +export const OUTPUT_PATH: string; +/** The SHA-256 of the vendored schema copy. */ +export function schemaDigest(): string; +/** The TypeScript type for one schema node. Throws on a keyword it does not know. */ +export function toType(node: unknown, path?: string, indent?: number): string; +/** The generated module's text for a schema and the source record it came from. */ +export function generate(schema: Record, source: { repository: string; path: string; commit: string; version: string }): string; +/** The `FORMATS` lines for the schema's `x-ichava-formats` registry; [] before 1.4.0. Throws on drift. */ +export function formatsBlock(schema: Record): string[]; diff --git a/.dev/browser/schema-types.mjs b/.dev/browser/schema-types.mjs new file mode 100644 index 0000000..55b8719 --- /dev/null +++ b/.dev/browser/schema-types.mjs @@ -0,0 +1,210 @@ +#!/usr/bin/env node +// Types for the icon-browser payloads, generated from its JSON Schema. +// +// icon-browser owns the schema (resources/schema/icon-browser.schema.json); this repo +// keeps a copy in schema/, with the commit it came from in schema/source.json, because +// the two are separate repositories and CI cannot read across them. The generated file +// is src/core/api/schema.gen.ts. Never edit it by hand. +// +// node scripts/schema-types.mjs write src/core/api/schema.gen.ts +// node scripts/schema-types.mjs --check fail when it differs from the schema (CI) +// +// To take a new schema: copy icon-browser's file over schema/icon-browser.schema.json, +// set `commit`, `version` and `sha256` in schema/source.json, and run the first command. +// `--check` fails when the copy's SHA-256 is not the recorded one, so a hand edit to the copy +// fails CI; the vitest suite also compares the copy with icon-browser's file when that +// repository is checked out beside this one (scripts/siblings.mjs). +// +// The generator covers the JSON Schema keywords that schema uses: type (one or a list), +// enum, const, $ref into $defs, properties/required, additionalProperties, items, +// allOf, anyOf and oneOf. A keyword it does not know fails the run, so a schema that +// grows a construct cannot be turned into a type that silently says less. The bounds +// keywords (minimum, minItems, uniqueItems) are accepted and dropped: TypeScript has no +// type for "an integer of at least 1", so they narrow values, never shapes. +// +// The root `x-ichava-formats` array (schema 1.4.0) is the code-tab and copy-format +// registry. It is emitted as `FORMATS`, in its own order, and its ids must equal +// `$defs/format.enum` exactly, or the run fails. Schema 1.6.0 adds the `asset` delivery and an +// optional `native: true` key, for exports the browser builds or points at rather than copies. + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Moved from react-icon-browser's scripts/ (react consolidation 3d, 2026-11-12). The schema is +// this repository's own now, resources/schema/; source.json, the record of the copy +// react-icon-browser vendored, stays with the imported root files. +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +export const SCHEMA_PATH = join(root, 'resources/schema/icon-browser.schema.json'); +export const SOURCE_PATH = join(root, '.dev/imported/react-icon-browser/schema/source.json'); + +/** The SHA-256 of the vendored schema copy, as recorded in schema/source.json. */ +export function schemaDigest() { + return createHash('sha256').update(readFileSync(SCHEMA_PATH)).digest('hex'); +} +export const OUTPUT_PATH = join(root, 'resources/js/core/api/schema.gen.ts'); + +/** Keywords that describe a value. Anything else in a schema node fails the run. */ +const KNOWN = new Set([ + 'type', 'enum', 'const', '$ref', 'properties', 'required', 'additionalProperties', 'items', + 'allOf', 'anyOf', 'oneOf', 'description', 'title', 'propertyNames', '$schema', '$id', 'version', '$defs', + 'minimum', 'minItems', 'uniqueItems', 'x-ichava-formats', +]); + +/** The keys every `x-ichava-formats` entry carries (contract §6.2). */ +const FORMAT_KEYS = ['id', 'label', 'group', 'delivery', 'kinds', 'copy', 'defaultCopy']; +/** Keys an entry may carry (schema 1.6.0): `native` marks an asset only a native toolchain builds. */ +const OPTIONAL_FORMAT_KEYS = ['native']; +const DELIVERIES = ['laravel', 'client', 'generated', 'asset']; + +/** + * The `FORMATS` constant for the schema's format registry, or nothing when the schema has + * none (before 1.4.0). Throws when the registry and `$defs/format` disagree, or an entry + * is malformed, so a registry that drifts cannot generate. + */ +export function formatsBlock(schema) { + const registry = schema['x-ichava-formats']; + if (registry === undefined) return []; + const enumIds = schema.$defs?.format?.enum; + if (!Array.isArray(registry) || !Array.isArray(enumIds)) throw new Error('x-ichava-formats needs $defs/format with an enum'); + const ids = registry.map((f) => f?.id); + if (JSON.stringify(ids) !== JSON.stringify(enumIds)) { + throw new Error(`x-ichava-formats ids [${ids.join(', ')}] differ from $defs/format.enum [${enumIds.join(', ')}]`); + } + for (const f of registry) { + const keys = Object.keys(f).filter((k) => !OPTIONAL_FORMAT_KEYS.includes(k)).sort(); + if (JSON.stringify(keys) !== JSON.stringify([...FORMAT_KEYS].sort())) throw new Error(`x-ichava-formats ${f.id}: keys must be ${FORMAT_KEYS.join(', ')}, and optionally ${OPTIONAL_FORMAT_KEYS.join(', ')}`); + if ('native' in f && f.native !== true) throw new Error(`x-ichava-formats ${f.id}: native must be true when present`); + if (!DELIVERIES.includes(f.delivery)) throw new Error(`x-ichava-formats ${f.id}: unknown delivery "${f.delivery}"`); + if (!Array.isArray(f.kinds) || !f.kinds.length) throw new Error(`x-ichava-formats ${f.id}: kinds must be a non-empty list`); + } + const row = (f) => ` { id: ${literal(f.id)}, label: ${literal(f.label)}, group: ${literal(f.group)}, delivery: ${literal(f.delivery)}, kinds: ${JSON.stringify(f.kinds)}, copy: ${f.copy}, defaultCopy: ${f.defaultCopy}${f.native ? ', native: true' : ''} },`; + return [ + '/** One entry of the format registry (`x-ichava-formats`). */', + 'export interface FormatEntry {', + ' id: Format;', + ' label: string;', + ' group: string;', + ` delivery: ${DELIVERIES.map(literal).join(' | ')};`, + ' kinds: readonly Kind[];', + ' copy: boolean;', + ' defaultCopy: boolean;', + ' /** An asset only a native toolchain builds (schema 1.6.0): the browser points at it, it cannot make it. */', + ' native?: true;', + '}', + '', + '/** The code-tab and copy-format registry (`x-ichava-formats`), in tab order. */', + 'export const FORMATS: readonly FormatEntry[] = [', + ...registry.map(row), + '];', + '', + ]; +} + +const pascal = (name) => name.replace(/(^|[_-])([a-z])/g, (_m, _s, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase()); +const literal = (v) => (v === null ? 'null' : JSON.stringify(v)); +const key = (k) => (/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)); +const union = (parts) => [...new Set(parts)].join(' | ') || 'unknown'; +const paren = (t) => (/[|&]/.test(t) ? `(${t})` : t); + +function objectType(node, path, indent) { + const props = node.properties ?? {}; + const required = new Set(node.required ?? []); + const pad = ' '.repeat(indent + 1); + const lines = Object.entries(props).map(([k, v]) => { + const doc = v && typeof v === 'object' && v.description ? `${pad}/** ${v.description} */\n` : ''; + return `${doc}${pad}${key(k)}${required.has(k) ? '' : '?'}: ${toType(v, `${path}.${k}`, indent + 1)};`; + }); + const extra = node.additionalProperties; + if (extra && typeof extra === 'object') lines.push(`${pad}[key: string]: ${toType(extra, `${path}[*]`, indent + 1)};`); + if (!lines.length) return extra === false ? 'Record' : 'Record'; + return `{\n${lines.join('\n')}\n${' '.repeat(indent)}}`; +} + +function typeFor(t, node, path, indent) { + switch (t) { + case 'string': return 'string'; + case 'integer': + case 'number': return 'number'; + case 'boolean': return 'boolean'; + case 'null': return 'null'; + case 'array': { + const items = node.items; + return `${paren(items && Object.keys(items).length ? toType(items, `${path}[]`, indent) : 'unknown')}[]`; + } + case 'object': { + // PHP serialises an empty map as [], so an "object" may also be listed as an array. + if (!node.properties && node.additionalProperties && typeof node.additionalProperties === 'object') { + return `Record`; + } + return objectType(node, path, indent); + } + default: throw new Error(`${path}: unknown type "${t}"`); + } +} + +export function toType(node, path = '#', indent = 0) { + if (node === true || node === undefined) return 'unknown'; + if (node === false) return 'never'; + for (const k of Object.keys(node)) if (!KNOWN.has(k)) throw new Error(`${path}: unsupported keyword "${k}"`); + if (node.$ref) { + const m = /^#\/\$defs\/(.+)$/.exec(node.$ref); + if (!m) throw new Error(`${path}: unsupported $ref ${node.$ref}`); + return pascal(m[1]); + } + if ('const' in node) return literal(node.const); + if (node.enum) return union(node.enum.map(literal)); + if (node.allOf) return node.allOf.map((s, i) => paren(toType(s, `${path}.allOf[${i}]`, indent))).join(' & '); + if (node.anyOf || node.oneOf) return union((node.anyOf ?? node.oneOf).map((s, i) => toType(s, `${path}.anyOf[${i}]`, indent))); + if (node.type === undefined) return node.properties ? objectType(node, path, indent) : 'unknown'; + const types = Array.isArray(node.type) ? node.type : [node.type]; + return union(types.map((t) => typeFor(t, node, path, indent))); +} + +/** The generated module's text for a schema and the source record it came from. */ +export function generate(schema, source) { + const out = [ + '// GENERATED by scripts/schema-types.mjs from schema/icon-browser.schema.json. Do not edit.', + `// Source: ${source.repository} ${source.path} at ${source.commit}.`, + '// Regenerate with `npm run schema:types`; CI fails when this file and the schema differ.', + '', + '/** The schema version these types describe (`schemaVersion` in the session and bootstrap documents). */', + `export const PAYLOAD_SCHEMA_VERSION = ${literal(schema.version ?? source.version)};`, + '', + ]; + for (const [name, def] of Object.entries(schema.$defs ?? {})) { + if (def.description) out.push(`/** ${def.description} */`); + out.push(`export type ${pascal(name)} = ${toType(def, `#/$defs/${name}`)};`, ''); + } + out.push(...formatsBlock(schema)); + return out.join('\n'); +} + +function main() { + const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')); + const source = JSON.parse(readFileSync(SOURCE_PATH, 'utf8')); + if (schema.version !== source.version) { + console.error(`schema/source.json says version ${source.version}, the schema says ${schema.version}.`); + process.exit(1); + } + if (source.sha256 !== schemaDigest()) { + console.error(`schema/icon-browser.schema.json has SHA-256 ${schemaDigest()}, schema/source.json records ${source.sha256}. Copy the file from icon-browser at the recorded commit; never edit it here.`); + process.exit(1); + } + const text = generate(schema, source); + if (process.argv.includes('--check')) { + let current = ''; + try { current = readFileSync(OUTPUT_PATH, 'utf8'); } catch { /* missing counts as drift */ } + if (current !== text) { + console.error('src/core/api/schema.gen.ts does not match schema/icon-browser.schema.json. Run `npm run schema:types`.'); + process.exit(1); + } + console.log(`schema types match (${Object.keys(schema.$defs ?? {}).length} definitions, schema ${schema.version}).`); + return; + } + writeFileSync(OUTPUT_PATH, text); + console.log(`wrote ${OUTPUT_PATH.slice(root.length + 1)} (${Object.keys(schema.$defs ?? {}).length} definitions).`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/.dev/browser/siblings.d.mts b/.dev/browser/siblings.d.mts new file mode 100644 index 0000000..9f6cd25 --- /dev/null +++ b/.dev/browser/siblings.d.mts @@ -0,0 +1,3 @@ +export function packagesDir(): string; +export function siblingPath(repository: string, path?: string): string | null; +export function parseRepoUrl(url: string): { repository: string; path: string } | null; diff --git a/.dev/browser/siblings.mjs b/.dev/browser/siblings.mjs new file mode 100644 index 0000000..a8f30ff --- /dev/null +++ b/.dev/browser/siblings.mjs @@ -0,0 +1,44 @@ +// Where another ichava repository's file is on this machine, when it is. +// +// A file this repository takes from another one is never read from a sibling checkout at build +// time: that path exists on one machine only. The convention, for every cross-repository path: +// +// - a COPY (content this repository builds or tests against) is vendored, with its +// repository, path, commit and SHA-256 recorded beside it, and a test pins the digest; +// - a LINK (prose or data pointing a reader at another repository's file) is the published +// URL, https://github.com/ichava//blob/main/; +// - a MANIFEST PATH (a file another repository keeps, read through the GitHub API) is the +// repository name plus the repository-relative path. +// +// The sibling checkout is only ever a cross-check: when it is on disk, a test compares the copy +// with it, or asserts the linked path exists in it, and fails on drift. When it is not (CI), the +// test is skipped, and the digest pin still holds. The checkouts are expected side by side under +// one packages directory; ICHAVA_PACKAGES_DIR names another one. + +import { existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); + +/** The directory holding the ichava checkouts side by side. */ +export function packagesDir() { + return resolve(process.env.ICHAVA_PACKAGES_DIR || join(root, '..')); +} + +/** + * The local path of `path` in the sibling checkout of `repository` (`ichava/icon-browser` or + * `icon-browser`), or null when that checkout is not on disk. + */ +export function siblingPath(repository, path = '') { + const name = repository.replace(/^ichava\//, ''); + const checkout = join(packagesDir(), name); + if (!existsSync(join(checkout, '.git'))) return null; + return join(checkout, path); +} + +/** `https://github.com/ichava//blob/main/` split into its repository and path, or null. */ +export function parseRepoUrl(url) { + const m = /^https:\/\/github\.com\/(ichava\/[A-Za-z0-9._-]+)\/blob\/main\/([^\s#?)"']+)/.exec(url); + return m ? { repository: m[1], path: decodeURIComponent(m[2]) } : null; +} diff --git a/.dev/browser/visual-drift.mjs b/.dev/browser/visual-drift.mjs new file mode 100644 index 0000000..94afc6f --- /dev/null +++ b/.dev/browser/visual-drift.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/** + * Prints a markdown table of how far each changed visual baseline moved. + * + * node scripts/visual-drift.mjs ... + * + * The report comes from a compare run (`--reporter=json`) taken before the baselines were + * regenerated, so its failures say how many pixels each one differs by. Used by + * visual-baselines.yml for its pull request comment and by `npm run visual:linux` locally, + * so both describe a change the same way. + */ +import { readFileSync } from 'node:fs'; + +const [report, ...changed] = process.argv.slice(2); +if (!report) { + console.error('usage: visual-drift.mjs ...'); + process.exit(2); +} + +/** "N pixels (ratio R ...)" and an optional size change, per snapshot name. */ +function driftFrom(json) { + const drift = {}; + const walk = (suite) => { + for (const spec of suite.specs ?? []) + for (const test of spec.tests ?? []) + for (const result of test.results ?? []) + for (const error of result.errors ?? []) { + const msg = error.message ?? ''; + const m = /(\d+) pixels \(ratio ([\d.]+)[^)]*\)[\s\S]*?Snapshot: (\S+\.png)/.exec(msg); + const size = /Expected an image (\d+px by \d+px), received (\d+px by \d+px)/.exec(msg); + if (m) drift[m[3]] = `${Number(m[1]).toLocaleString('en')} px (${(Number(m[2]) * 100).toFixed(0)}%)` + (size ? `, size ${size[1]} to ${size[2]}` : ''); + } + for (const child of suite.suites ?? []) walk(child); + }; + for (const suite of json.suites ?? []) walk(suite); + return drift; +} + +let drift = {}; +try { + drift = driftFrom(JSON.parse(readFileSync(report, 'utf8'))); +} catch (e) { + console.error(`visual-drift: no usable report (${e.message}); sizes are unmeasured`); +} + +const rows = changed.map((file) => { + const name = file.split('/').pop(); + const key = name.replace(/-chromium-linux\.png$/, '.png'); + return `| \`${name}\` | ${drift[key] ?? 'new or unmeasured'} |`; +}); +console.log(['| Baseline | Pixels changed |', '|---|---|', ...rows].join('\n')); diff --git a/.dev/browser/visual-linux.sh b/.dev/browser/visual-linux.sh new file mode 100755 index 0000000..b940f6d --- /dev/null +++ b/.dev/browser/visual-linux.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Runs inside the Playwright image for scripts/linux.sh, against a copy of the checkout. +# +# Mounting the checkout and installing in place failed two ways. In a git worktree `.git` is a +# pointer file to a directory outside the mount, so npm's git lookup for a git dependency died +# with "not a git repository". And `npm install` in the container replaced the host's +# node_modules with Linux binaries, breaking the next build on the Mac. +# +# So the copy leaves out .git, node_modules, build output and agent worktrees, the install and +# the run happen in the copy, and only the regenerated snapshot folders come back. +# +# Modes: visual-update (regenerate, copy back, print the drift table), visual-check (compare +# only), e2e (the behavioural suite). A first argument that is not a mode is read as +# visual-update, which is what this script did before it took modes. +set -eu + +case "${1:-}" in + visual-update|visual-check|e2e) mode=$1; shift ;; + *) mode=visual-update ;; +esac + +src=${VISUAL_SRC:-/src} +work=${VISUAL_WORK:-$(mktemp -d)} + +tar -C "$src" \ + --exclude=./.git --exclude=./node_modules --exclude=./dist --exclude=./.claude \ + --exclude=./test-results --exclude=./playwright-report \ + -cf - . | tar -C "$work" -xf - + +cd "$work" +npm install --no-audit --no-fund + +status=0 +case "$mode" in + e2e) + npx playwright test "$@" || status=$? + ;; + visual-check) + npx playwright test --config=playwright.visual.config.ts "$@" || status=$? + ;; + visual-update) + # Measure first, so the summary can say how far each baseline moved. + npx playwright test --config=playwright.visual.config.ts --reporter=json "$@" > "$work/.visual-drift.json" 2>/dev/null || true + npx playwright test --config=playwright.visual.config.ts --update-snapshots "$@" || status=$? + changed="" + for f in e2e/*-snapshots/*.png; do + [ -f "$f" ] || continue + if ! cmp -s "$f" "$src/$f"; then changed="$changed $f"; fi + done + for dir in e2e/*-snapshots; do + if [ -d "$dir" ]; then cp -R "$dir" "$src/e2e/"; fi + done + if [ -n "$changed" ]; then + echo + echo "Changed baselines (review them with git diff before committing):" + # shellcheck disable=SC2086 # one path per word, by construction + # The table is a courtesy: a missing report must not fail a run that regenerated fine. + node scripts/visual-drift.mjs "$work/.visual-drift.json" $changed || echo "(drift table unavailable)" + else + echo "Baselines already match; nothing changed." + fi + ;; +esac + +exit "$status" diff --git a/.dev/browser/write-prepaint.mjs b/.dev/browser/write-prepaint.mjs new file mode 100644 index 0000000..ddca853 --- /dev/null +++ b/.dev/browser/write-prepaint.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// `npm run build:prepaint`: write resources/vendor/react-icon-browser/prepaint.html from the +// browser's source, resources/js/boot/prepaint.html, with the "Browser" suffix. +// +// That file is the one part of the deprecated vendored directory still read at run time: +// the component (src/View/Components/Prepaint.php) inlines +// it. react-icon-browser's library build emitted it (emitPrepaint) and the sync copied it here; +// since react consolidation 3d (2026-11-12) the source lives in this repository, so this writes +// it in place. resources/js/core/prepaint.test.ts fails while the two disagree. +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { prepaintFragment } from './prepaint.mjs'; + +const target = fileURLToPath(new URL('../../resources/vendor/react-icon-browser/prepaint.html', import.meta.url)); +writeFileSync(target, `${prepaintFragment({ suffix: 'Browser' })}\n`); +console.log('wrote resources/vendor/react-icon-browser/prepaint.html'); diff --git a/.dev/imported/README.md b/.dev/imported/README.md new file mode 100644 index 0000000..58fc97b --- /dev/null +++ b/.dev/imported/README.md @@ -0,0 +1,15 @@ +# Imported root files + +`react-icon-browser/` holds the root files of `ichava/react-icon-browser` as they were at +`3cdbd1bc8689bd29772b50bbe3a4585112497fd2`, imported as a snapshot with no history on 2026-11-12 +(react consolidation 3d). Its `src/` is `resources/js/`, its `scripts/` is `.dev/browser/` and its +fonts are `resources/assets/fonts/`; what is left here is kept, not built: the standalone and +embedded pages, its configs, docs, e2e suite and visual baselines, and its package manifest. + +Its `LICENSE` is that repository's and is superseded here by this repository's root +[LICENSE](../../LICENSE): the Untitled UI PRO components the browser uses are included in this +product under the Untitled UI licence (owner, 2026-11-12). The 20 Metronic paths react-icon-browser +holds were not imported, and its `.github/` was left out. `src/test/setup.ts` is the copy that was +merged by hand into `resources/js/test/setup.ts`. + +Nothing here is removed without the owner's yes. diff --git a/.dev/imported/react-icon-browser/.dependency-cruiser.cjs b/.dev/imported/react-icon-browser/.dependency-cruiser.cjs new file mode 100644 index 0000000..0fe4990 --- /dev/null +++ b/.dev/imported/react-icon-browser/.dependency-cruiser.cjs @@ -0,0 +1,203 @@ +/** + * Layer-boundary enforcement. + * + * The layering is load-bearing: it is what makes a later Vue or Blade consumer + * possible, and it only stays true if something checks. `core/` already leaked + * React precisely because nothing did. + * + * app/ routes, entry points, host adapters -> ui, data, core + * ui/ components + thin bindings -> core + * data/ store, schemas, sources -> core; no react + * core/ pure TS engines -> nothing + * + * Directory-to-layer mapping, until the layers become real packages: + * core src/core/** + * data src/store.ts, src/data/** + * ui src/components/**, src/hooks/** + * app src/{App,main,mount,embedded,IchavaBrowser}.tsx, src/app/** (see APP below) + * + * Every rule below is TRUE as of the commit that added this file. Do not add an + * exception to make a violation pass -- fix the import, or move the file. + */ +/** + * The app layer, defined once. + * + * Listing these by hand in each rule does not scale: adding `embedded.tsx` broke + * `app-is-a-leaf`, because one app entry importing another is legitimate and the + * hand-written exception list did not know about the new file. + */ +// src/app/ is the composition root architecture.md 3.2 names (the browser() builder first). +const APP = '^src/(App|main|mount|embedded|IchavaBrowser)\\.tsx$|^src/app/'; +/** The package barrel re-exports the public surface, entry points included. */ +const BARREL = '^src/index\\.ts$'; + +module.exports = { + forbidden: [ + { + name: 'core-no-react', + comment: + 'core/ must not import React, INCLUDING type-only imports. A CSSProperties in a ' + + 'return type puts React into the public surface a Vue or Blade consumer inherits. ' + + 'Use the plain StyleObject from core/types instead, and put the hook in hooks/.', + severity: 'error', + from: { path: '^src/core/' }, + // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must + // be written against node_modules. Written as '^react$' the rule silently never + // fires -- which is how it shipped the first time, and why every rule here has a + // deliberate-violation proof in the commit that added it. + to: { path: '^node_modules/react(-dom)?(/|$)' }, + }, + { + name: 'core-is-a-leaf', + comment: + 'core/ must not depend on any other layer. Test fixtures are the one exception: ' + + 'src/test/ is shared scaffolding, not a layer.', + severity: 'error', + from: { path: '^src/core/', pathNot: '\\.test\\.tsx?$' }, + to: { path: `^src/(components|hooks|store\\.ts)|${APP}` }, + }, + { + name: 'core-tests-stay-in-core', + comment: + 'A test under core/ that imports a component is testing the wrong layer -- move the ' + + 'test next to what it covers. src/test/ fixtures are allowed.', + severity: 'error', + from: { path: '^src/core/.*\\.test\\.tsx?$' }, + to: { path: '^src/(components|hooks)/' }, + }, + { + name: 'data-no-react', + comment: 'The store and data layer must stay portable: no React imports.', + severity: 'error', + from: { path: '^src/(store\\.ts|data/)' }, + // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must + // be written against node_modules. Written as '^react$' the rule silently never + // fires -- which is how it shipped the first time, and why every rule here has a + // deliberate-violation proof in the commit that added it. + to: { path: '^node_modules/react(-dom)?(/|$)' }, + }, + { + name: 'ui-not-into-app', + comment: 'Components take props; the app wires them. ui/ must not reach into app/.', + severity: 'error', + from: { path: '^src/(components|hooks)/' }, + to: { path: APP }, + }, + { + name: 'app-is-a-leaf', + comment: + 'Nothing may import the app layer. src/index.ts is exempt: it is the package barrel, ' + + 'and re-exporting the public surface -- entry points included -- is exactly its job. ' + + 'That is a distribution concern, not a layer. A test file is exempt too, the same way ' + + "core-is-a-leaf exempts core's own tests below: a test for an app-layer component is " + + 'necessarily an app-layer test, and the alternative is not testing that layer at all.', + severity: 'error', + from: { pathNot: `${APP}|${BARREL}|\\.test\\.tsx?$` }, + to: { path: APP }, + }, + /* + * The bounded-context modules (architecture.md 3.2). Nothing lives under + * src/modules/ yet: these rules land before the code they govern, and + * scripts/check-boundary-rules.mjs plants a violation of each one on every CI run + * and fails unless each fires, so a rule matching nothing cannot pass silently. + * + * ui -> app -> application -> domain; infrastructure -> application, domain + * a module reaches another module only through its index.ts + * state/ (the zustand slices) may use domain and application, never ui + */ + { + name: 'module-domain-is-plain-ts', + comment: + 'A module domain imports no framework: no React, zustand, react-aria or Untitled UI, ' + + 'type-only imports included. The domain tsconfig drops DOM from lib for the same reason.', + severity: 'error', + from: { path: '^src/modules/[^/]+/domain/' }, + to: { path: '^node_modules/(react|react-dom|zustand|react-aria|react-aria-components|@react-aria|@react-stately|@react-types|@untitledui)(/|$)' }, + }, + { + name: 'module-domain-is-innermost', + comment: 'A domain depends on nothing outward: not its own application or infrastructure, not ui, state or the app.', + severity: 'error', + from: { path: '^src/modules/[^/]+/domain/' }, + to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/(application|infrastructure)/|${APP}` }, + }, + { + name: 'module-application-not-outward', + comment: 'Application services use the domain and ports; adapters, ui, state and React stay outside.', + severity: 'error', + from: { path: '^src/modules/[^/]+/application/' }, + to: { + path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/infrastructure/|${APP}|^node_modules/(react|react-dom|zustand)(/|$)`, + }, + }, + { + name: 'module-infrastructure-not-into-ui', + comment: 'Adapters implement ports; they never reach ui, state, the app or React.', + severity: 'error', + from: { path: '^src/modules/[^/]+/infrastructure/' }, + to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|${APP}|^node_modules/(react|react-dom)(/|$)` }, + }, + { + name: 'modules-meet-through-index', + comment: + "One module reaches another only through that module's index.ts, so a module's " + + 'internals can move without breaking its neighbours.', + severity: 'error', + from: { path: '^src/modules/([^/]+)/' }, + to: { path: '^src/modules/[^/]+/', pathNot: ['^src/modules/$1/', '^src/modules/[^/]+/index\\.ts$'] }, + }, + { + name: 'state-not-into-ui', + comment: 'The store slices hold state and call domain and application code; they never import ui.', + severity: 'error', + from: { path: '^src/state/' }, + to: { path: `^src/(components|hooks|ui)/|${APP}|^node_modules/(react|react-dom)(/|$)` }, + }, + { + name: 'no-circular', + comment: 'A dependency cycle makes the layering unprovable.', + severity: 'error', + from: {}, + to: { circular: true }, + }, + { + name: 'no-orphans', + comment: + 'An unreachable module is dead code. Warn only: entry points, configs and type-only ' + + 'declaration files legitimately have no importers.', + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)\\.[^/]+\\.(cjs|mjs|js|ts)$', + '\\.d\\.ts$', + '(^|/)tsconfig\\.[^/]+\\.json$', + '^src/(main|mount|embedded)\\.tsx$', + ], + }, + to: {}, + }, + { + name: 'no-unreachable', + comment: + 'A module no entry point reaches is dead code, even when it imports things itself. ' + + 'no-orphans only sees modules with no edges at all, so a dead file that imported ' + + 'clsx (src/lib/utils.ts) passed it. Tests, declaration files and configs are exempt.', + severity: 'error', + from: { path: '^src/(main|mount|embedded)\\.tsx$|^src/index\\.ts$' }, + to: { + path: '^src/', + pathNot: ['\\.test\\.tsx?$', '^src/test/', '\\.d\\.ts$', '^src/(main|mount|embedded)\\.tsx$', '^src/index\\.ts$'], + reachable: false, + }, + }, + ], + + options: { + doNotFollow: { path: 'node_modules' }, + tsPreCompilationDeps: true, // so type-only imports are seen -- the whole point of core-no-react + tsConfig: { fileName: 'tsconfig.app.json' }, + enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['import', 'require', 'types'] }, + reporterOptions: { text: { highlightFocused: true } }, + }, +}; diff --git a/.dev/imported/react-icon-browser/.editorconfig b/.dev/imported/react-icon-browser/.editorconfig new file mode 100644 index 0000000..85f3717 --- /dev/null +++ b/.dev/imported/react-icon-browser/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.dev/imported/react-icon-browser/.gitattributes b/.dev/imported/react-icon-browser/.gitattributes new file mode 100644 index 0000000..f254ff3 --- /dev/null +++ b/.dev/imported/react-icon-browser/.gitattributes @@ -0,0 +1,18 @@ +# Force LF so Windows clones do not break scripts or snapshots. +*.sh text eol=lf +*.ts text +*.tsx text +*.json text +*.yml text +*.yaml text +*.css text + +# Playwright baselines are binary; never normalise them or every visual +# comparison fails for a reason that has nothing to do with the UI. +*.png -text +*.svg -text + +# Same reasoning, one step earlier in the chain: a normalised woff2 is a corrupt +# woff2, the browser silently falls back, and the damage shows up as a screenshot +# diff that blames the UI. +*.woff2 -text diff --git a/.dev/imported/react-icon-browser/.gitignore b/.dev/imported/react-icon-browser/.gitignore new file mode 100644 index 0000000..2ace54d --- /dev/null +++ b/.dev/imported/react-icon-browser/.gitignore @@ -0,0 +1,18 @@ +node_modules/ +dist/ +dist-ssr/ +*.local +.DS_Store +.vite/ +*.tsbuildinfo + +/test-results/ +/playwright-report/ + +# No lock file is committed (ecosystem rule). +package-lock.json + +# Agent worktrees and local design notes. Tailwind's source scan honours this file but not +# a global git excludes file, so without this line it reads every checkout under +# .claude/worktrees/ and the library build can carry the classes they use. +/.claude/ diff --git a/.dev/imported/react-icon-browser/.release-please-manifest.json b/.dev/imported/react-icon-browser/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.dev/imported/react-icon-browser/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/.dev/imported/react-icon-browser/CHANGELOG.md b/.dev/imported/react-icon-browser/CHANGELOG.md new file mode 100644 index 0000000..264e603 --- /dev/null +++ b/.dev/imported/react-icon-browser/CHANGELOG.md @@ -0,0 +1,1051 @@ +# Changelog + +All notable changes to `ichava/react-icon-browser` are documented here. The format is +based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- **The "Cookies & storage" notice moves.** Drag it by the grip at its top, with a mouse, pen or finger, or focus the grip ("Move notice", described for screen readers) and use the arrow keys, Shift for bigger steps and Home to put it back. It stays 8px inside the window while dragged and on every resize, and when the window gets too small to hold it anywhere it goes back to its corner. Its place lasts for the session (`sessionStorage`, `.position`, every access in try/catch). The offset is the CSS `translate` property, so the entrance animation (which owns `transform`) cannot hold a moved notice at its corner, and keyboard steps glide unless motion is reduced (the OS setting or the in-app toggle). Tab order is grip, "Essential only", "Accept all". `consent.move` and `consent.moveHint` in all 7 languages. Unit tests (`useDraggableNotice.test.ts`, `IchavaBrowser.consentDrag.test.tsx`) and `e2e/consent-drag.spec.ts` (mouse, keyboard, touch, clamp, resize, reload). + +- **Motion preview: pause holds the frame, and the browser tears js-motion down.** `MotionEngine` gains `pauseMotion()`, `resumeMotion()` (js-motion `pauseAll`/`resumeAll`) and `destroyMotion()` (`destroy()`). The detail dialog's pause button no longer re-runs the preview effect, so resume continues from where it paused instead of frame 0, and a preset or easing chosen while paused starts paused. `destroyMotion()` runs when the dialog closes and when the browser unmounts, so a host that mounts and unmounts it leaks no listener. **The browser plays through its own engine (`IchavaMotion.create()`), not the default one**: `pauseAll()` and `destroy()` on the shared default would pause a host page's animations and unwire its `autoInit()` elements. The Motion snippet also emits the data-attribute form (`data-ichava-motion`, `data-motion-trigger`, `data-motion-duration`, `data-motion-easing`, and `data-motion-direction` when not normal) for `IchavaMotion.autoInit()`; JSON · Custom has no preset id and does not get it. `SnippetMotion` gains an optional `custom`. Reduced motion stops motion, now with tests (the in-app setting, and the OS preference changing at runtime). `@ichava/js-motion` is pinned to `ef495cd` (ichava/js-motion#45, `pauseAll`, `resumeAll`, `destroy`, `data-motion-duration`) until the re-squash re-points it to `#v0.1.0`; `audits/resquash-checklist.md` section 3.1, row S11. + +- **Payload schema 1.6.0: Qwik, Ember and Elm tabs, three asset exports and three native entries** (ichava/icon-browser `8c4fafe`, #128). The vendored schema and `schema/source.json` move to 1.6.0, and `scripts/schema-types.mjs` accepts the new `asset` delivery and the optional `native: true` key, refusing any other value; `PAYLOAD_SCHEMA_VERSION` is `1.6.0`. + - **Code tabs and copy formats `qwik`, `ember` and `elm`.** Qwik imports `IconHome` from `@ichava/js-icon-components/qwik/` with the Tier 2 props; Ember imports from `…/ember/` into a `.gjs` `