diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 97a0cd33ac74..52c07d0778e6 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -42,6 +42,7 @@ "profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review", "sync:pierre-icons": "node modules/t3-markdown-text/scripts/sync-pierre-file-icons.mjs", "generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-uniwind-themes.mts", + "generate:lucide-icons": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-environment-lucide-icons.mts", "test": "vp test run --config ../../vite.config.ts --dir .", "typecheck": "tsc --noEmit" }, diff --git a/apps/mobile/scripts/generate-environment-lucide-icons.mts b/apps/mobile/scripts/generate-environment-lucide-icons.mts new file mode 100644 index 000000000000..24e0761e43af --- /dev/null +++ b/apps/mobile/scripts/generate-environment-lucide-icons.mts @@ -0,0 +1,90 @@ +import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodeModule from "node:module"; +import * as NodePath from "node:path"; +import { ENVIRONMENT_LUCIDE_ICON_IDS } from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; + +const GENERATED_MODULE_PATH = NodePath.resolve( + import.meta.dirname, + "../src/components/environmentLucideIcons.generated.ts", +); + +// Path data comes from the same lucide-react build web draws, resolved from +// the web workspace so both clients stay on one icon version. +const webRequire = NodeModule.createRequire( + NodePath.resolve(import.meta.dirname, "../../web/package.json"), +); +const lucideRoot = NodePath.dirname(webRequire.resolve("lucide-react/package.json")); +const lucideVersion = Schema.decodeUnknownSync( + Schema.fromJsonString(Schema.Struct({ version: Schema.String })), +)(NodeFS.readFileSync(NodePath.join(lucideRoot, "package.json"), "utf8")).version; + +const decodeIconNode = Schema.decodeUnknownSync( + Schema.fromJsonString( + Schema.Array(Schema.Tuple([Schema.String, Schema.Record(Schema.String, Schema.String)])), + ), +); + +type IconNode = ReadonlyArray>]>; + +/** + * Reads the `__iconNode` array out of one lucide-react icon module. The + * module is plain ESM with a single array literal, parsed as JSON after + * quoting its keys, so nothing is executed. + */ +function readIconNode(id: string): IconNode { + const source = NodeFS.readFileSync( + NodePath.join(lucideRoot, "dist/esm/icons", `${id}.js`), + "utf8", + ); + const literal = /const __iconNode = (\[[\s\S]*?\]);\n/u.exec(source)?.[1]; + if (literal === undefined) throw new Error(`lucide-react icon ${id} has no __iconNode literal.`); + let parsed: ReturnType; + try { + parsed = decodeIconNode(literal.replaceAll(/(\w+): /gu, '"$1": ')); + } catch (cause) { + throw new Error(`lucide-react icon ${id} has an unexpected node.`, { cause }); + } + // React needs the key only to reconcile a list; mobile builds a static tree. + return parsed.map(([element, { key: _key, ...attributes }]) => [element, attributes]); +} + +/** + * The module text, passed through the repository formatter so the committed + * file matches what the pre-commit hook would write and the freshness test + * compares like with like. + */ +export function renderEnvironmentLucideIconsModule(): string { + const entries = ENVIRONMENT_LUCIDE_ICON_IDS.map( + (id) => ` "${id}": ${JSON.stringify(readIconNode(id))},`, + ); + const raw = [ + "// Generated by scripts/generate-environment-lucide-icons.mts. Do not edit.", + `// Source: lucide-react ${lucideVersion}, ids from ENVIRONMENT_LUCIDE_ICON_IDS in contracts.`, + 'import type { EnvironmentLucideIconId } from "@t3tools/contracts";', + "", + "export type LucideIconNode = ReadonlyArray<", + " readonly [element: string, attributes: Readonly>]", + ">;", + "", + "export const ENVIRONMENT_LUCIDE_ICON_NODES: Record = {", + ...entries, + "};", + "", + ].join("\n"); + // The `vp` bin is a shell shim, so it runs through the shell, not node. + const formatted = NodeChildProcess.spawnSync( + NodePath.resolve(import.meta.dirname, "../../../node_modules/.bin/vp"), + ["fmt", `--stdin-filepath=${GENERATED_MODULE_PATH}`], + { input: raw, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }, + ); + if (formatted.status !== 0) { + throw new Error(`vp fmt failed while formatting the generated module: ${formatted.stderr}`); + } + return formatted.stdout; +} + +if (import.meta.main) { + NodeFS.writeFileSync(GENERATED_MODULE_PATH, renderEnvironmentLucideIconsModule()); +} diff --git a/apps/mobile/scripts/generate-environment-lucide-icons.test.ts b/apps/mobile/scripts/generate-environment-lucide-icons.test.ts new file mode 100644 index 000000000000..002e4675db62 --- /dev/null +++ b/apps/mobile/scripts/generate-environment-lucide-icons.test.ts @@ -0,0 +1,18 @@ +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import { describe, expect, it } from "vite-plus/test"; + +import { renderEnvironmentLucideIconsModule } from "./generate-environment-lucide-icons.mts"; + +describe("generate environment Lucide icons", () => { + it("keeps the committed module current", () => { + const generated = NodePath.resolve( + import.meta.dirname, + "../src/components/environmentLucideIcons.generated.ts", + ); + expect( + NodeFS.existsSync(generated) ? NodeFS.readFileSync(generated, "utf8") : null, + "Run `vp run --filter @t3tools/mobile generate:lucide-icons` and commit the output.", + ).toBe(renderEnvironmentLucideIconsModule()); + }); +}); diff --git a/apps/mobile/src/components/EnvironmentLucideIcon.tsx b/apps/mobile/src/components/EnvironmentLucideIcon.tsx new file mode 100644 index 000000000000..e68d126b77f4 --- /dev/null +++ b/apps/mobile/src/components/EnvironmentLucideIcon.tsx @@ -0,0 +1,96 @@ +import type { EnvironmentLucideIconId } from "@t3tools/contracts"; +import { memo } from "react"; +import Svg, { Circle, Ellipse, Line, Path, Polygon, Polyline, Rect } from "react-native-svg"; +import { withUniwind } from "uniwind"; + +import { + ENVIRONMENT_LUCIDE_ICON_NODES, + type LucideIconNode, +} from "./environmentLucideIcons.generated"; + +const ThemedSvg = withUniwind(Svg); + +/** + * Draws one of the shared Lucide icons from generated path data, on the same + * 24 unit grid and 2 unit stroke web uses. Only the element kinds Lucide + * ships are handled, and the generator emits only those kinds. + */ +function renderNode(node: LucideIconNode) { + // The node lists are static, so position is a stable key. + return node.map(([element, attributes], key) => { + // A handful of icons fill a dot with currentColor; everything else strokes. + const fill = attributes.fill === "currentColor" ? "currentColor" : "none"; + switch (element) { + case "path": + return ; + case "circle": + return ( + + ); + case "rect": + return ( + + ); + case "line": + return ( + + ); + case "polyline": + return ; + case "polygon": + return ; + case "ellipse": + return ( + + ); + default: + return null; + } + }); +} + +export const EnvironmentLucideIcon = memo(function EnvironmentLucideIcon(props: { + readonly id: EnvironmentLucideIconId; + readonly size: number; + readonly colorClassName: string; + readonly accessibilityLabel?: string; +}) { + return ( + + {renderNode(ENVIRONMENT_LUCIDE_ICON_NODES[props.id])} + + ); +}); diff --git a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx index fff1a57c3883..69da182d73e0 100644 --- a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx +++ b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx @@ -1,12 +1,14 @@ import { ENVIRONMENT_ICON_LABELS, isEnvironmentCuratedIconId, + isEnvironmentLucideIconId, type EnvironmentCuratedIconId, type EnvironmentIcon, } from "@t3tools/contracts"; import { projectIconColorClassNames } from "../lib/projectIcon"; import { SymbolView, type AppSymbolName } from "./AppSymbol"; +import { EnvironmentLucideIcon } from "./EnvironmentLucideIcon"; import { ProjectIconGlyphView } from "./ProjectFavicon"; // Every SF name here is already a key of the Android fallback map, so a new @@ -71,17 +73,33 @@ export function EnvironmentMachineSymbol(props: { /> ); } + const tintColorClassName = + icon.kind === "icon" && icon.color !== undefined + ? projectIconColorClassNames(icon.color).tint + : props.tintColorClassName; + // Curated first, then the shared Lucide list; the generated module holds + // path data for exactly those ids, so anything else is the generic server. + if ( + icon.kind === "icon" && + !isEnvironmentCuratedIconId(icon.name) && + isEnvironmentLucideIconId(icon.name) + ) { + return ( + + ); + } const id = symbolId(icon); return ( ); diff --git a/apps/mobile/src/components/environmentLucideIcons.generated.ts b/apps/mobile/src/components/environmentLucideIcons.generated.ts new file mode 100644 index 000000000000..8427cf8cfcb9 --- /dev/null +++ b/apps/mobile/src/components/environmentLucideIcons.generated.ts @@ -0,0 +1,1090 @@ +// Generated by scripts/generate-environment-lucide-icons.mts. Do not edit. +// Source: lucide-react 0.564.0, ids from ENVIRONMENT_LUCIDE_ICON_IDS in contracts. +import type { EnvironmentLucideIconId } from "@t3tools/contracts"; + +export type LucideIconNode = ReadonlyArray< + readonly [element: string, attributes: Readonly>] +>; + +export const ENVIRONMENT_LUCIDE_ICON_NODES: Record = { + activity: [ + [ + "path", + { + d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2", + }, + ], + ], + anchor: [ + ["path", { d: "M12 6v16" }], + ["path", { d: "m19 13 2-1a9 9 0 0 1-18 0l2 1" }], + ["path", { d: "M9 11h6" }], + ["circle", { cx: "12", cy: "4", r: "2" }], + ], + antenna: [ + ["path", { d: "M2 12 7 2" }], + ["path", { d: "m7 12 5-10" }], + ["path", { d: "m12 12 5-10" }], + ["path", { d: "m17 12 5-10" }], + ["path", { d: "M4.5 7h15" }], + ["path", { d: "M12 16v6" }], + ], + beaker: [ + ["path", { d: "M4.5 3h15" }], + ["path", { d: "M6 3v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V3" }], + ["path", { d: "M6 14h12" }], + ], + bike: [ + ["circle", { cx: "18.5", cy: "17.5", r: "3.5" }], + ["circle", { cx: "5.5", cy: "17.5", r: "3.5" }], + ["circle", { cx: "15", cy: "5", r: "1" }], + ["path", { d: "M12 17.5V14l-3-3 4-3 2 3h2" }], + ], + binary: [ + ["rect", { x: "14", y: "14", width: "4", height: "6", rx: "2" }], + ["rect", { x: "6", y: "4", width: "4", height: "6", rx: "2" }], + ["path", { d: "M6 20h4" }], + ["path", { d: "M14 10h4" }], + ["path", { d: "M6 14h2v6" }], + ["path", { d: "M14 4h2v6" }], + ], + blocks: [ + [ + "path", + { + d: "M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2", + }, + ], + ["rect", { x: "14", y: "2", width: "8", height: "8", rx: "1" }], + ], + bot: [ + ["path", { d: "M12 8V4H8" }], + ["rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }], + ["path", { d: "M2 14h2" }], + ["path", { d: "M20 14h2" }], + ["path", { d: "M15 13v2" }], + ["path", { d: "M9 13v2" }], + ], + boxes: [ + [ + "path", + { + d: "M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z", + }, + ], + ["path", { d: "m7 16.5-4.74-2.85" }], + ["path", { d: "m7 16.5 5-3" }], + ["path", { d: "M7 16.5v5.17" }], + [ + "path", + { + d: "M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z", + }, + ], + ["path", { d: "m17 16.5-5-3" }], + ["path", { d: "m17 16.5 4.74-2.85" }], + ["path", { d: "M17 16.5v5.17" }], + [ + "path", + { + d: "M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z", + }, + ], + ["path", { d: "M12 8 7.26 5.15" }], + ["path", { d: "m12 8 4.74-2.85" }], + ["path", { d: "M12 13.5V8" }], + ], + braces: [ + ["path", { d: "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1" }], + ["path", { d: "M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" }], + ], + "brain-circuit": [ + [ + "path", + { d: "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" }, + ], + ["path", { d: "M9 13a4.5 4.5 0 0 0 3-4" }], + ["path", { d: "M6.003 5.125A3 3 0 0 0 6.401 6.5" }], + ["path", { d: "M3.477 10.896a4 4 0 0 1 .585-.396" }], + ["path", { d: "M6 18a4 4 0 0 1-1.967-.516" }], + ["path", { d: "M12 13h4" }], + ["path", { d: "M12 18h6a2 2 0 0 1 2 2v1" }], + ["path", { d: "M12 8h8" }], + ["path", { d: "M16 8V5a2 2 0 0 1 2-2" }], + ["circle", { cx: "16", cy: "13", r: ".5" }], + ["circle", { cx: "18", cy: "3", r: ".5" }], + ["circle", { cx: "20", cy: "21", r: ".5" }], + ["circle", { cx: "20", cy: "8", r: ".5" }], + ], + bug: [ + ["path", { d: "M12 20v-9" }], + ["path", { d: "M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" }], + ["path", { d: "M14.12 3.88 16 2" }], + ["path", { d: "M21 21a4 4 0 0 0-3.81-4" }], + ["path", { d: "M21 5a4 4 0 0 1-3.55 3.97" }], + ["path", { d: "M22 13h-4" }], + ["path", { d: "M3 21a4 4 0 0 1 3.81-4" }], + ["path", { d: "M3 5a4 4 0 0 0 3.55 3.97" }], + ["path", { d: "M6 13H2" }], + ["path", { d: "m8 2 1.88 1.88" }], + ["path", { d: "M9 7.13V6a3 3 0 1 1 6 0v1.13" }], + ], + building: [ + ["path", { d: "M12 10h.01" }], + ["path", { d: "M12 14h.01" }], + ["path", { d: "M12 6h.01" }], + ["path", { d: "M16 10h.01" }], + ["path", { d: "M16 14h.01" }], + ["path", { d: "M16 6h.01" }], + ["path", { d: "M8 10h.01" }], + ["path", { d: "M8 14h.01" }], + ["path", { d: "M8 6h.01" }], + ["path", { d: "M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" }], + ["rect", { x: "4", y: "2", width: "16", height: "20", rx: "2" }], + ], + "building-2": [ + ["path", { d: "M10 12h4" }], + ["path", { d: "M10 8h4" }], + ["path", { d: "M14 21v-3a2 2 0 0 0-4 0v3" }], + ["path", { d: "M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2" }], + ["path", { d: "M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16" }], + ], + bus: [ + ["path", { d: "M8 6v6" }], + ["path", { d: "M15 6v6" }], + ["path", { d: "M2 12h19.6" }], + [ + "path", + { + d: "M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3", + }, + ], + ["circle", { cx: "7", cy: "18", r: "2" }], + ["path", { d: "M9 18h5" }], + ["circle", { cx: "16", cy: "18", r: "2" }], + ], + cable: [ + ["path", { d: "M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z" }], + ["path", { d: "M17 21v-2" }], + ["path", { d: "M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10" }], + ["path", { d: "M21 21v-2" }], + ["path", { d: "M3 5V3" }], + ["path", { d: "M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z" }], + ["path", { d: "M7 5V3" }], + ], + camera: [ + [ + "path", + { + d: "M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z", + }, + ], + ["circle", { cx: "12", cy: "13", r: "3" }], + ], + car: [ + [ + "path", + { + d: "M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2", + }, + ], + ["circle", { cx: "7", cy: "17", r: "2" }], + ["path", { d: "M9 17h6" }], + ["circle", { cx: "17", cy: "17", r: "2" }], + ], + castle: [ + ["path", { d: "M10 5V3" }], + ["path", { d: "M14 5V3" }], + ["path", { d: "M15 21v-3a3 3 0 0 0-6 0v3" }], + ["path", { d: "M18 3v8" }], + ["path", { d: "M18 5H6" }], + ["path", { d: "M22 11H2" }], + ["path", { d: "M22 9v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9" }], + ["path", { d: "M6 3v8" }], + ], + cctv: [ + [ + "path", + { d: "M16.75 12h3.632a1 1 0 0 1 .894 1.447l-2.034 4.069a1 1 0 0 1-1.708.134l-2.124-2.97" }, + ], + [ + "path", + { + d: "M17.106 9.053a1 1 0 0 1 .447 1.341l-3.106 6.211a1 1 0 0 1-1.342.447L3.61 12.3a2.92 2.92 0 0 1-1.3-3.91L3.69 5.6a2.92 2.92 0 0 1 3.92-1.3z", + }, + ], + ["path", { d: "M2 19h3.76a2 2 0 0 0 1.8-1.1L9 15" }], + ["path", { d: "M2 21v-4" }], + ["path", { d: "M7 9h.01" }], + ], + "chart-line": [ + ["path", { d: "M3 3v16a2 2 0 0 0 2 2h16" }], + ["path", { d: "m19 9-5 5-4-4-3 3" }], + ], + "cloud-cog": [ + ["path", { d: "m10.852 19.772-.383.924" }], + ["path", { d: "m13.148 14.228.383-.923" }], + ["path", { d: "M13.148 19.772a3 3 0 1 0-2.296-5.544l-.383-.923" }], + ["path", { d: "m13.53 20.696-.382-.924a3 3 0 1 1-2.296-5.544" }], + ["path", { d: "m14.772 15.852.923-.383" }], + ["path", { d: "m14.772 18.148.923.383" }], + ["path", { d: "M4.2 15.1a7 7 0 1 1 9.93-9.858A7 7 0 0 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.2" }], + ["path", { d: "m9.228 15.852-.923-.383" }], + ["path", { d: "m9.228 18.148-.923.383" }], + ], + "cloud-download": [ + ["path", { d: "M12 13v8l-4-4" }], + ["path", { d: "m12 21 4-4" }], + ["path", { d: "M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" }], + ], + "cloud-upload": [ + ["path", { d: "M12 13v8" }], + ["path", { d: "M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" }], + ["path", { d: "m8 17 4-4 4 4" }], + ], + code: [ + ["path", { d: "m16 18 6-6-6-6" }], + ["path", { d: "m8 6-6 6 6 6" }], + ], + "code-xml": [ + ["path", { d: "m18 16 4-4-4-4" }], + ["path", { d: "m6 8-4 4 4 4" }], + ["path", { d: "m14.5 4-5 16" }], + ], + coffee: [ + ["path", { d: "M10 2v2" }], + ["path", { d: "M14 2v2" }], + [ + "path", + { + d: "M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1", + }, + ], + ["path", { d: "M6 2v2" }], + ], + cog: [ + ["path", { d: "M11 10.27 7 3.34" }], + ["path", { d: "m11 13.73-4 6.93" }], + ["path", { d: "M12 22v-2" }], + ["path", { d: "M12 2v2" }], + ["path", { d: "M14 12h8" }], + ["path", { d: "m17 20.66-1-1.73" }], + ["path", { d: "m17 3.34-1 1.73" }], + ["path", { d: "M2 12h2" }], + ["path", { d: "m20.66 17-1.73-1" }], + ["path", { d: "m20.66 7-1.73 1" }], + ["path", { d: "m3.34 17 1.73-1" }], + ["path", { d: "m3.34 7 1.73 1" }], + ["circle", { cx: "12", cy: "12", r: "2" }], + ["circle", { cx: "12", cy: "12", r: "8" }], + ], + compass: [ + [ + "path", + { + d: "m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z", + }, + ], + ["circle", { cx: "12", cy: "12", r: "10" }], + ], + computer: [ + ["rect", { width: "14", height: "8", x: "5", y: "2", rx: "2" }], + ["rect", { width: "20", height: "8", x: "2", y: "14", rx: "2" }], + ["path", { d: "M6 18h2" }], + ["path", { d: "M12 18h6" }], + ], + cpu: [ + ["path", { d: "M12 20v2" }], + ["path", { d: "M12 2v2" }], + ["path", { d: "M17 20v2" }], + ["path", { d: "M17 2v2" }], + ["path", { d: "M2 12h2" }], + ["path", { d: "M2 17h2" }], + ["path", { d: "M2 7h2" }], + ["path", { d: "M20 12h2" }], + ["path", { d: "M20 17h2" }], + ["path", { d: "M20 7h2" }], + ["path", { d: "M7 20v2" }], + ["path", { d: "M7 2v2" }], + ["rect", { x: "4", y: "4", width: "16", height: "16", rx: "2" }], + ["rect", { x: "8", y: "8", width: "8", height: "8", rx: "1" }], + ], + "database-zap": [ + ["ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }], + ["path", { d: "M3 5V19A9 3 0 0 0 15 21.84" }], + ["path", { d: "M21 5V8" }], + ["path", { d: "M21 12L18 17H22L19 22" }], + ["path", { d: "M3 12A9 3 0 0 0 14.59 14.87" }], + ], + "disc-3": [ + ["circle", { cx: "12", cy: "12", r: "10" }], + ["path", { d: "M6 12c0-1.7.7-3.2 1.8-4.2" }], + ["circle", { cx: "12", cy: "12", r: "2" }], + ["path", { d: "M18 12c0 1.7-.7 3.2-1.8 4.2" }], + ], + drill: [ + ["path", { d: "M10 18a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3 1 1 0 0 1 1-1z" }], + [ + "path", + { + d: "M13 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1l-.81 3.242a1 1 0 0 1-.97.758H8", + }, + ], + ["path", { d: "M14 4h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-3" }], + ["path", { d: "M18 6h4" }], + ["path", { d: "m5 10-2 8" }], + ["path", { d: "m7 18 2-8" }], + ], + earth: [ + ["path", { d: "M21.54 15H17a2 2 0 0 0-2 2v4.54" }], + [ + "path", + { d: "M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17" }, + ], + ["path", { d: "M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05" }], + ["circle", { cx: "12", cy: "12", r: "10" }], + ], + "ethernet-port": [ + [ + "path", + { d: "m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3z" }, + ], + ["path", { d: "M6 8v1" }], + ["path", { d: "M10 8v1" }], + ["path", { d: "M14 8v1" }], + ["path", { d: "M18 8v1" }], + ], + factory: [ + ["path", { d: "M12 16h.01" }], + ["path", { d: "M16 16h.01" }], + [ + "path", + { + d: "M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z", + }, + ], + ["path", { d: "M8 16h.01" }], + ], + fan: [ + [ + "path", + { + d: "M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z", + }, + ], + ["path", { d: "M12 12v.01" }], + ], + "file-code": [ + [ + "path", + { + d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z", + }, + ], + ["path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }], + ["path", { d: "M10 12.5 8 15l2 2.5" }], + ["path", { d: "m14 12.5 2 2.5-2 2.5" }], + ], + film: [ + ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], + ["path", { d: "M7 3v18" }], + ["path", { d: "M3 7.5h4" }], + ["path", { d: "M3 12h18" }], + ["path", { d: "M3 16.5h4" }], + ["path", { d: "M17 3v18" }], + ["path", { d: "M17 7.5h4" }], + ["path", { d: "M17 16.5h4" }], + ], + flag: [ + [ + "path", + { + d: "M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528", + }, + ], + ], + flame: [ + [ + "path", + { + d: "M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4", + }, + ], + ], + "flask-conical": [ + [ + "path", + { + d: "M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2", + }, + ], + ["path", { d: "M6.453 15h11.094" }], + ["path", { d: "M8.5 2h7" }], + ], + "folder-code": [ + ["path", { d: "M10 10.5 8 13l2 2.5" }], + ["path", { d: "m14 10.5 2 2.5-2 2.5" }], + [ + "path", + { + d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z", + }, + ], + ], + "gamepad-2": [ + ["line", { x1: "6", x2: "10", y1: "11", y2: "11" }], + ["line", { x1: "8", x2: "8", y1: "9", y2: "13" }], + ["line", { x1: "15", x2: "15.01", y1: "12", y2: "12" }], + ["line", { x1: "18", x2: "18.01", y1: "10", y2: "10" }], + [ + "path", + { + d: "M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z", + }, + ], + ], + gauge: [ + ["path", { d: "m12 14 4-4" }], + ["path", { d: "M3.34 19a10 10 0 1 1 17.32 0" }], + ], + ghost: [ + ["path", { d: "M9 10h.01" }], + ["path", { d: "M15 10h.01" }], + ["path", { d: "M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z" }], + ], + "git-branch": [ + ["path", { d: "M15 6a9 9 0 0 0-9 9V3" }], + ["circle", { cx: "18", cy: "6", r: "3" }], + ["circle", { cx: "6", cy: "18", r: "3" }], + ], + "git-merge": [ + ["circle", { cx: "18", cy: "18", r: "3" }], + ["circle", { cx: "6", cy: "6", r: "3" }], + ["path", { d: "M6 21V9a9 9 0 0 0 9 9" }], + ], + "globe-lock": [ + ["path", { d: "M15.686 15A14.5 14.5 0 0 1 12 22a14.5 14.5 0 0 1 0-20 10 10 0 1 0 9.542 13" }], + ["path", { d: "M2 12h8.5" }], + ["path", { d: "M20 6V4a2 2 0 1 0-4 0v2" }], + ["rect", { width: "8", height: "5", x: "14", y: "6", rx: "1" }], + ], + hammer: [ + ["path", { d: "m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9" }], + ["path", { d: "m18 15 4-4" }], + [ + "path", + { + d: "m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5", + }, + ], + ], + "hard-drive": [ + ["path", { d: "M10 16h.01" }], + [ + "path", + { + d: "M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z", + }, + ], + ["path", { d: "M21.946 12.013H2.054" }], + ["path", { d: "M6 16h.01" }], + ], + "hard-drive-download": [ + ["path", { d: "M12 2v8" }], + ["path", { d: "m16 6-4 4-4-4" }], + ["rect", { width: "20", height: "8", x: "2", y: "14", rx: "2" }], + ["path", { d: "M6 18h.01" }], + ["path", { d: "M10 18h.01" }], + ], + headphones: [ + [ + "path", + { + d: "M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3", + }, + ], + ], + heart: [ + [ + "path", + { + d: "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5", + }, + ], + ], + hospital: [ + ["path", { d: "M12 7v4" }], + ["path", { d: "M14 21v-3a2 2 0 0 0-4 0v3" }], + ["path", { d: "M14 9h-4" }], + ["path", { d: "M18 11h2a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2" }], + ["path", { d: "M18 21V5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16" }], + ], + "house-plug": [ + ["path", { d: "M10 12V8.964" }], + ["path", { d: "M14 12V8.964" }], + ["path", { d: "M15 12a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2a1 1 0 0 1 1-1z" }], + [ + "path", + { + d: "M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2", + }, + ], + ], + "house-wifi": [ + ["path", { d: "M9.5 13.866a4 4 0 0 1 5 .01" }], + ["path", { d: "M12 17h.01" }], + [ + "path", + { + d: "M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", + }, + ], + ["path", { d: "M7 10.754a8 8 0 0 1 10 0" }], + ], + joystick: [ + ["path", { d: "M21 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2Z" }], + ["path", { d: "M6 15v-2" }], + ["path", { d: "M12 15V9" }], + ["circle", { cx: "12", cy: "6", r: "3" }], + ], + "key-round": [ + [ + "path", + { + d: "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z", + }, + ], + ["circle", { cx: "16.5", cy: "7.5", r: ".5", fill: "currentColor" }], + ], + keyboard: [ + ["path", { d: "M10 8h.01" }], + ["path", { d: "M12 12h.01" }], + ["path", { d: "M14 8h.01" }], + ["path", { d: "M16 12h.01" }], + ["path", { d: "M18 8h.01" }], + ["path", { d: "M6 8h.01" }], + ["path", { d: "M7 16h10" }], + ["path", { d: "M8 12h.01" }], + ["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }], + ], + landmark: [ + ["path", { d: "M10 18v-7" }], + [ + "path", + { + d: "M11.12 2.198a2 2 0 0 1 1.76.006l7.866 3.847c.476.233.31.949-.22.949H3.474c-.53 0-.695-.716-.22-.949z", + }, + ], + ["path", { d: "M14 18v-7" }], + ["path", { d: "M18 18v-7" }], + ["path", { d: "M3 22h18" }], + ["path", { d: "M6 18v-7" }], + ], + "laptop-minimal": [ + ["rect", { width: "18", height: "12", x: "3", y: "4", rx: "2", ry: "2" }], + ["line", { x1: "2", x2: "22", y1: "20", y2: "20" }], + ], + layers: [ + [ + "path", + { + d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z", + }, + ], + ["path", { d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" }], + ["path", { d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" }], + ], + link: [ + ["path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }], + ["path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }], + ], + lock: [ + ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2" }], + ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }], + ], + "map-pin": [ + [ + "path", + { + d: "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0", + }, + ], + ["circle", { cx: "12", cy: "10", r: "3" }], + ], + "memory-stick": [ + ["path", { d: "M12 12v-2" }], + ["path", { d: "M12 18v-2" }], + ["path", { d: "M16 12v-2" }], + ["path", { d: "M16 18v-2" }], + ["path", { d: "M2 11h1.5" }], + ["path", { d: "M20 18v-2" }], + ["path", { d: "M20.5 11H22" }], + ["path", { d: "M4 18v-2" }], + ["path", { d: "M8 12v-2" }], + ["path", { d: "M8 18v-2" }], + ["rect", { x: "2", y: "6", width: "20", height: "10", rx: "2" }], + ], + mic: [ + ["path", { d: "M12 19v3" }], + ["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }], + ["rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" }], + ], + microchip: [ + ["path", { d: "M10 12h4" }], + ["path", { d: "M10 17h4" }], + ["path", { d: "M10 7h4" }], + ["path", { d: "M18 12h2" }], + ["path", { d: "M18 18h2" }], + ["path", { d: "M18 6h2" }], + ["path", { d: "M4 12h2" }], + ["path", { d: "M4 18h2" }], + ["path", { d: "M4 6h2" }], + ["rect", { x: "6", y: "2", width: "12", height: "20", rx: "2" }], + ], + "monitor-cog": [ + ["path", { d: "M12 17v4" }], + ["path", { d: "m14.305 7.53.923-.382" }], + ["path", { d: "m15.228 4.852-.923-.383" }], + ["path", { d: "m16.852 3.228-.383-.924" }], + ["path", { d: "m16.852 8.772-.383.923" }], + ["path", { d: "m19.148 3.228.383-.924" }], + ["path", { d: "m19.53 9.696-.382-.924" }], + ["path", { d: "m20.772 4.852.924-.383" }], + ["path", { d: "m20.772 7.148.924.383" }], + ["path", { d: "M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }], + ["path", { d: "M8 21h8" }], + ["circle", { cx: "18", cy: "6", r: "3" }], + ], + "monitor-smartphone": [ + ["path", { d: "M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8" }], + ["path", { d: "M10 19v-3.96 3.15" }], + ["path", { d: "M7 19h5" }], + ["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2" }], + ], + moon: [ + [ + "path", + { + d: "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401", + }, + ], + ], + mountain: [["path", { d: "m8 3 4 8 5-5 5 15H2L8 3z" }]], + mouse: [ + ["rect", { x: "5", y: "2", width: "14", height: "20", rx: "7" }], + ["path", { d: "M12 6v4" }], + ], + package: [ + [ + "path", + { + d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z", + }, + ], + ["path", { d: "M12 22V12" }], + ["polyline", { points: "3.29 7 12 12 20.71 7" }], + ["path", { d: "m7.5 4.27 9 5.15" }], + ], + "pc-case": [ + ["rect", { width: "14", height: "20", x: "5", y: "2", rx: "2" }], + ["path", { d: "M15 14h.01" }], + ["path", { d: "M9 6h6" }], + ["path", { d: "M9 10h6" }], + ], + pickaxe: [ + ["path", { d: "m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999" }], + [ + "path", + { + d: "M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024", + }, + ], + [ + "path", + { + d: "M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069", + }, + ], + [ + "path", + { + d: "M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z", + }, + ], + ], + plane: [ + [ + "path", + { + d: "M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z", + }, + ], + ], + plug: [ + ["path", { d: "M12 22v-5" }], + ["path", { d: "M15 8V2" }], + ["path", { d: "M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z" }], + ["path", { d: "M9 8V2" }], + ], + power: [ + ["path", { d: "M12 2v10" }], + ["path", { d: "M18.4 6.6a9 9 0 1 1-12.77.04" }], + ], + printer: [ + ["path", { d: "M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" }], + ["path", { d: "M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" }], + ["rect", { x: "6", y: "14", width: "12", height: "8", rx: "1" }], + ], + puzzle: [ + [ + "path", + { + d: "M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z", + }, + ], + ], + radio: [ + ["path", { d: "M16.247 7.761a6 6 0 0 1 0 8.478" }], + ["path", { d: "M19.075 4.933a10 10 0 0 1 0 14.134" }], + ["path", { d: "M4.925 19.067a10 10 0 0 1 0-14.134" }], + ["path", { d: "M7.753 16.239a6 6 0 0 1 0-8.478" }], + ["circle", { cx: "12", cy: "12", r: "2" }], + ], + "radio-tower": [ + ["path", { d: "M4.9 16.1C1 12.2 1 5.8 4.9 1.9" }], + ["path", { d: "M7.8 4.7a6.14 6.14 0 0 0-.8 7.5" }], + ["circle", { cx: "12", cy: "9", r: "2" }], + ["path", { d: "M16.2 4.8c2 2 2.26 5.11.8 7.47" }], + ["path", { d: "M19.1 1.9a9.96 9.96 0 0 1 0 14.1" }], + ["path", { d: "M9.5 18h5" }], + ["path", { d: "m8 22 4-11 4 11" }], + ], + rocket: [ + [ + "path", + { + d: "M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z", + }, + ], + [ + "path", + { + d: "m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z", + }, + ], + ["path", { d: "M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" }], + ["path", { d: "M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" }], + ], + router: [ + ["rect", { width: "20", height: "8", x: "2", y: "14", rx: "2" }], + ["path", { d: "M6.01 18H6" }], + ["path", { d: "M10.01 18H10" }], + ["path", { d: "M15 10v4" }], + ["path", { d: "M17.84 7.17a4 4 0 0 0-5.66 0" }], + ["path", { d: "M20.66 4.34a8 8 0 0 0-11.31 0" }], + ], + satellite: [ + [ + "path", + { + d: "m13.5 6.5-3.148-3.148a1.205 1.205 0 0 0-1.704 0L6.352 5.648a1.205 1.205 0 0 0 0 1.704L9.5 10.5", + }, + ], + ["path", { d: "M16.5 7.5 19 5" }], + [ + "path", + { + d: "m17.5 10.5 3.148 3.148a1.205 1.205 0 0 1 0 1.704l-2.296 2.296a1.205 1.205 0 0 1-1.704 0L13.5 14.5", + }, + ], + ["path", { d: "M9 21a6 6 0 0 0-6-6" }], + [ + "path", + { + d: "M9.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l4.296-4.296a1.205 1.205 0 0 0 0-1.704l-2.296-2.296a1.205 1.205 0 0 0-1.704 0z", + }, + ], + ], + "satellite-dish": [ + ["path", { d: "M4 10a7.31 7.31 0 0 0 10 10Z" }], + ["path", { d: "m9 15 3-3" }], + ["path", { d: "M17 13a6 6 0 0 0-6-6" }], + ["path", { d: "M21 13A10 10 0 0 0 11 3" }], + ], + school: [ + ["path", { d: "M14 21v-3a2 2 0 0 0-4 0v3" }], + ["path", { d: "M18 5v16" }], + ["path", { d: "m4 6 7.106-3.79a2 2 0 0 1 1.788 0L20 6" }], + [ + "path", + { + d: "m6 11-3.52 2.147a1 1 0 0 0-.48.854V19a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a1 1 0 0 0-.48-.853L18 11", + }, + ], + ["path", { d: "M6 5v16" }], + ["circle", { cx: "12", cy: "9", r: "2" }], + ], + "server-cog": [ + ["path", { d: "m10.852 14.772-.383.923" }], + ["path", { d: "M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923" }], + ["path", { d: "m13.148 9.228.383-.923" }], + ["path", { d: "m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544" }], + ["path", { d: "m14.772 10.852.923-.383" }], + ["path", { d: "m14.772 13.148.923.383" }], + ["path", { d: "M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5" }], + [ + "path", + { d: "M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5" }, + ], + ["path", { d: "M6 18h.01" }], + ["path", { d: "M6 6h.01" }], + ["path", { d: "m9.228 10.852-.923-.383" }], + ["path", { d: "m9.228 13.148-.923.383" }], + ], + "server-crash": [ + ["path", { d: "M6 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2" }], + ["path", { d: "M6 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2" }], + ["path", { d: "M6 6h.01" }], + ["path", { d: "M6 18h.01" }], + ["path", { d: "m13 6-4 6h6l-4 6" }], + ], + "server-off": [ + ["path", { d: "M7 2h13a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-5" }], + ["path", { d: "M10 10 2.5 2.5C2 2 2 2.5 2 5v3a2 2 0 0 0 2 2h6z" }], + ["path", { d: "M22 17v-1a2 2 0 0 0-2-2h-1" }], + ["path", { d: "M4 14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16.5l1-.5.5.5-8-8H4z" }], + ["path", { d: "M6 18h.01" }], + ["path", { d: "m2 2 20 20" }], + ], + "share-2": [ + ["circle", { cx: "18", cy: "5", r: "3" }], + ["circle", { cx: "6", cy: "12", r: "3" }], + ["circle", { cx: "18", cy: "19", r: "3" }], + ["line", { x1: "8.59", x2: "15.42", y1: "13.51", y2: "17.49" }], + ["line", { x1: "15.41", x2: "8.59", y1: "6.51", y2: "10.49" }], + ], + shield: [ + [ + "path", + { + d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z", + }, + ], + ], + "shield-check": [ + [ + "path", + { + d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z", + }, + ], + ["path", { d: "m9 12 2 2 4-4" }], + ], + ship: [ + ["path", { d: "M12 10.189V14" }], + ["path", { d: "M12 2v3" }], + ["path", { d: "M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6" }], + [ + "path", + { + d: "M19.38 20A11.6 11.6 0 0 0 21 14l-8.188-3.639a2 2 0 0 0-1.624 0L3 14a11.6 11.6 0 0 0 2.81 7.76", + }, + ], + [ + "path", + { + d: "M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1s1.2 1 2.5 1c2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1", + }, + ], + ], + skull: [ + ["path", { d: "m12.5 17-.5-1-.5 1h1z" }], + [ + "path", + { + d: "M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z", + }, + ], + ["circle", { cx: "15", cy: "12", r: "1" }], + ["circle", { cx: "9", cy: "12", r: "1" }], + ], + smartphone: [ + ["rect", { width: "14", height: "20", x: "5", y: "2", rx: "2", ry: "2" }], + ["path", { d: "M12 18h.01" }], + ], + snowflake: [ + ["path", { d: "m10 20-1.25-2.5L6 18" }], + ["path", { d: "M10 4 8.75 6.5 6 6" }], + ["path", { d: "m14 20 1.25-2.5L18 18" }], + ["path", { d: "m14 4 1.25 2.5L18 6" }], + ["path", { d: "m17 21-3-6h-4" }], + ["path", { d: "m17 3-3 6 1.5 3" }], + ["path", { d: "M2 12h6.5L10 9" }], + ["path", { d: "m20 10-1.5 2 1.5 2" }], + ["path", { d: "M22 12h-6.5L14 15" }], + ["path", { d: "m4 10 1.5 2L4 14" }], + ["path", { d: "m7 21 3-6-1.5-3" }], + ["path", { d: "m7 3 3 6h4" }], + ], + sparkles: [ + [ + "path", + { + d: "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z", + }, + ], + ["path", { d: "M20 2v4" }], + ["path", { d: "M22 4h-4" }], + ["circle", { cx: "4", cy: "20", r: "2" }], + ], + speaker: [ + ["rect", { width: "16", height: "20", x: "4", y: "2", rx: "2" }], + ["path", { d: "M12 6h.01" }], + ["circle", { cx: "12", cy: "14", r: "4" }], + ["path", { d: "M12 14h.01" }], + ], + "square-terminal": [ + ["path", { d: "m7 11 2-2-2-2" }], + ["path", { d: "M11 13h4" }], + ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }], + ], + star: [ + [ + "path", + { + d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z", + }, + ], + ], + store: [ + ["path", { d: "M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5" }], + [ + "path", + { + d: "M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244", + }, + ], + ["path", { d: "M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05" }], + ], + sun: [ + ["circle", { cx: "12", cy: "12", r: "4" }], + ["path", { d: "M12 2v2" }], + ["path", { d: "M12 20v2" }], + ["path", { d: "m4.93 4.93 1.41 1.41" }], + ["path", { d: "m17.66 17.66 1.41 1.41" }], + ["path", { d: "M2 12h2" }], + ["path", { d: "M20 12h2" }], + ["path", { d: "m6.34 17.66-1.41 1.41" }], + ["path", { d: "m19.07 4.93-1.41 1.41" }], + ], + tablet: [ + ["rect", { width: "16", height: "20", x: "4", y: "2", rx: "2", ry: "2" }], + ["line", { x1: "12", x2: "12.01", y1: "18", y2: "18" }], + ], + tent: [ + ["path", { d: "M3.5 21 14 3" }], + ["path", { d: "M20.5 21 10 3" }], + ["path", { d: "M15.5 21 12 15l-3.5 6" }], + ["path", { d: "M2 21h20" }], + ], + "test-tube": [ + ["path", { d: "M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2" }], + ["path", { d: "M8.5 2h7" }], + ["path", { d: "M14.5 16h-5" }], + ], + thermometer: [["path", { d: "M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" }]], + "train-front": [ + ["path", { d: "M8 3.1V7a4 4 0 0 0 8 0V3.1" }], + ["path", { d: "m9 15-1-1" }], + ["path", { d: "m15 15 1-1" }], + ["path", { d: "M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z" }], + ["path", { d: "m8 19-2 3" }], + ["path", { d: "m16 19 2 3" }], + ], + trees: [ + ["path", { d: "M10 10v.2A3 3 0 0 1 8.9 16H5a3 3 0 0 1-1-5.8V10a3 3 0 0 1 6 0Z" }], + ["path", { d: "M7 16v6" }], + ["path", { d: "M13 19v3" }], + [ + "path", + { + d: "M12 19h8.3a1 1 0 0 0 .7-1.7L18 14h.3a1 1 0 0 0 .7-1.7L16 9h.2a1 1 0 0 0 .8-1.7L13 3l-1.4 1.5", + }, + ], + ], + truck: [ + ["path", { d: "M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2" }], + ["path", { d: "M15 18H9" }], + [ + "path", + { d: "M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14" }, + ], + ["circle", { cx: "17", cy: "18", r: "2" }], + ["circle", { cx: "7", cy: "18", r: "2" }], + ], + tv: [ + ["path", { d: "m17 2-5 5-5-5" }], + ["rect", { width: "20", height: "15", x: "2", y: "7", rx: "2" }], + ], + unplug: [ + ["path", { d: "m19 5 3-3" }], + ["path", { d: "m2 22 3-3" }], + ["path", { d: "M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z" }], + ["path", { d: "M7.5 13.5 10 11" }], + ["path", { d: "M10.5 16.5 13 14" }], + ["path", { d: "m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z" }], + ], + usb: [ + ["circle", { cx: "10", cy: "7", r: "1" }], + ["circle", { cx: "4", cy: "20", r: "1" }], + ["path", { d: "M4.7 19.3 19 5" }], + ["path", { d: "m21 3-3 1 2 2Z" }], + ["path", { d: "M9.26 7.68 5 12l2 5" }], + ["path", { d: "m10 14 5 2 3.5-3.5" }], + ["path", { d: "m18 12 1-1 1 1-1 1Z" }], + ], + warehouse: [ + ["path", { d: "M18 21V10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v11" }], + [ + "path", + { + d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 1.132-1.803l7.95-3.974a2 2 0 0 1 1.837 0l7.948 3.974A2 2 0 0 1 22 8z", + }, + ], + ["path", { d: "M6 13h12" }], + ["path", { d: "M6 17h12" }], + ], + watch: [ + ["path", { d: "M12 10v2.2l1.6 1" }], + ["path", { d: "m16.13 7.66-.81-4.05a2 2 0 0 0-2-1.61h-2.68a2 2 0 0 0-2 1.61l-.78 4.05" }], + ["path", { d: "m7.88 16.36.8 4a2 2 0 0 0 2 1.61h2.72a2 2 0 0 0 2-1.61l.81-4.05" }], + ["circle", { cx: "12", cy: "12", r: "6" }], + ], + wifi: [ + ["path", { d: "M12 20h.01" }], + ["path", { d: "M2 8.82a15 15 0 0 1 20 0" }], + ["path", { d: "M5 12.859a10 10 0 0 1 14 0" }], + ["path", { d: "M8.5 16.429a5 5 0 0 1 7 0" }], + ], + workflow: [ + ["rect", { width: "8", height: "8", x: "3", y: "3", rx: "2" }], + ["path", { d: "M7 11v4a2 2 0 0 0 2 2h4" }], + ["rect", { width: "8", height: "8", x: "13", y: "13", rx: "2" }], + ], + wrench: [ + [ + "path", + { + d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z", + }, + ], + ], + zap: [ + [ + "path", + { + d: "M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z", + }, + ], + ], +}; diff --git a/apps/web/src/components/EnvironmentMachineIcon.tsx b/apps/web/src/components/EnvironmentMachineIcon.tsx index 4d14292a483b..dc40747bc24a 100644 --- a/apps/web/src/components/EnvironmentMachineIcon.tsx +++ b/apps/web/src/components/EnvironmentMachineIcon.tsx @@ -1,5 +1,6 @@ import { isEnvironmentCuratedIconId, + isEnvironmentLucideIconId, type EnvironmentCuratedIconId, type EnvironmentIcon, } from "@t3tools/contracts"; @@ -17,7 +18,8 @@ import { TerminalIcon, type LucideProps, } from "lucide-react"; -import type { FunctionComponent, SVGProps } from "react"; +import type { IconName } from "lucide-react/dynamic"; +import { lazy, Suspense, type FunctionComponent, type SVGProps } from "react"; import { cn } from "~/lib/utils"; import { projectIconColorClassName } from "../projectIconColors"; import { LinuxIcon } from "./Icons"; @@ -63,6 +65,13 @@ function MacStudioIcon(props: SVGProps) { ); } +// Resolves a Lucide id at runtime and code-splits each icon into its own +// chunk, so none of the ~1,900 reach the main bundle; ProjectFavicon does the +// same for project icons. +const DynamicIcon = lazy(() => + import("lucide-react/dynamic").then((module) => ({ default: module.DynamicIcon })), +); + const ICON_BY_ID: Record> = { server: ServerIcon, cloud: CloudIcon, @@ -127,14 +136,35 @@ export function EnvironmentMachineIcon({ if (icon.kind === "monogram") { return ; } - const Icon = ICON_BY_ID[curatedIconId(icon)]; const color = icon.kind === "icon" && icon.color !== undefined ? icon.color : undefined; - return ( - - ); + const coloredClassName = + color === undefined ? className : cn(className, projectIconColorClassName(color)); + // Curated first, then the shared Lucide list, else the generic server. While + // a Lucide glyph loads, the slot holds the server glyph at the same size so + // the row does not shift. + if ( + icon.kind === "icon" && + !isEnvironmentCuratedIconId(icon.name) && + isEnvironmentLucideIconId(icon.name) + ) { + const placeholder = ; + return ( + + placeholder} + /> + + ); + } + const Icon = ICON_BY_ID[curatedIconId(icon)]; + return ; } const COMPONENT_BY_ICON = new WeakMap< diff --git a/apps/web/src/components/settings/EnvironmentIconPicker.logic.ts b/apps/web/src/components/settings/EnvironmentIconPicker.logic.ts index 232cbfb95328..d8925c629bc6 100644 --- a/apps/web/src/components/settings/EnvironmentIconPicker.logic.ts +++ b/apps/web/src/components/settings/EnvironmentIconPicker.logic.ts @@ -1,10 +1,12 @@ import { + ENVIRONMENT_LUCIDE_ICON_IDS, environmentIconForCuratedId, isEnvironmentMachineKind, isMonogramLength, MonogramText, type EnvironmentCuratedIconId, type EnvironmentIcon, + type EnvironmentLucideIconId, type EnvironmentMachineKind, type IconColor, type ServerConfig, @@ -87,20 +89,30 @@ function normalizeMonogram(value: string): string { export function resolveEnvironmentIconDialogWrite(input: { readonly mode: EnvironmentIconDialogMode; readonly iconId: EnvironmentCuratedIconId; + /** A pick from the shared Lucide list, which takes precedence over `iconId`. */ + readonly lucideId: EnvironmentLucideIconId | null; readonly color: IconColor | null; readonly emoji: string; readonly monogram: string; readonly detected: EnvironmentMachineKind; }): EnvironmentIconDialogWrite { switch (input.mode) { - case "icon": + case "icon": { + if (input.lucideId === null && input.color === null) { + return { + kind: "write", + icon: resolveNamedIconWrite({ next: input.iconId, detected: input.detected }), + }; + } + const name = input.lucideId ?? input.iconId; return { kind: "write", icon: input.color === null - ? resolveNamedIconWrite({ next: input.iconId, detected: input.detected }) - : { kind: "icon", name: input.iconId, color: input.color }, + ? { kind: "icon", name } + : { kind: "icon", name, color: input.color }, }; + } case "emoji": return firstEmoji(input.emoji) === input.emoji ? { kind: "write", icon: { kind: "emoji", emoji: input.emoji } } @@ -120,3 +132,16 @@ export function resolveEnvironmentIconDialogWrite(input: { } } } + +/** The shared Lucide ids whose name contains every word of `query`. */ +export function filterEnvironmentLucideIconIds( + query: string, +): ReadonlyArray { + const words = query + .trim() + .toLowerCase() + .split(/\s+/u) + .filter((word) => word.length > 0); + if (words.length === 0) return ENVIRONMENT_LUCIDE_ICON_IDS; + return ENVIRONMENT_LUCIDE_ICON_IDS.filter((id) => words.every((word) => id.includes(word))); +} diff --git a/apps/web/src/components/settings/EnvironmentIconPicker.test.ts b/apps/web/src/components/settings/EnvironmentIconPicker.test.ts index 77aa10f20b9b..96bf439a15b5 100644 --- a/apps/web/src/components/settings/EnvironmentIconPicker.test.ts +++ b/apps/web/src/components/settings/EnvironmentIconPicker.test.ts @@ -1,7 +1,12 @@ -import { environmentIconForMachineKind, type ServerConfig } from "@t3tools/contracts"; +import { + ENVIRONMENT_LUCIDE_ICON_IDS, + environmentIconForMachineKind, + type ServerConfig, +} from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { + filterEnvironmentLucideIconIds, resolveEnvironmentIconChoiceLock, resolveEnvironmentIconDialogWrite, resolveEnvironmentIconPickerLock, @@ -70,6 +75,7 @@ describe("resolveEnvironmentIconChoiceLock", () => { describe("resolveEnvironmentIconDialogWrite", () => { const base = { iconId: "laptop", + lucideId: null, color: null, emoji: "🚀", monogram: "K8", @@ -91,6 +97,16 @@ describe("resolveEnvironmentIconDialogWrite", () => { ).toEqual({ kind: "write", icon: { kind: "icon", name: "database" } }); }); + it("prefers a pick from the shared Lucide list over the curated grid", () => { + expect(resolveEnvironmentIconDialogWrite({ ...base, mode: "icon", lucideId: "cpu" })).toEqual({ + kind: "write", + icon: { kind: "icon", name: "cpu" }, + }); + expect( + resolveEnvironmentIconDialogWrite({ ...base, mode: "icon", lucideId: "cpu", color: "sky" }), + ).toEqual({ kind: "write", icon: { kind: "icon", name: "cpu", color: "sky" } }); + }); + it("keeps a colored pick of the detected kind, since the color is the point", () => { expect( resolveEnvironmentIconDialogWrite({ ...base, mode: "icon", iconId: "server", color: "red" }), @@ -127,3 +143,16 @@ describe("resolveEnvironmentIconDialogWrite", () => { ).toBe("invalid"); }); }); + +describe("filterEnvironmentLucideIconIds", () => { + it("matches every word of the query against the id and keeps list order", () => { + expect(filterEnvironmentLucideIconIds("")).toBe(ENVIRONMENT_LUCIDE_ICON_IDS); + expect(filterEnvironmentLucideIconIds("server")).toEqual([ + "server-cog", + "server-crash", + "server-off", + ]); + expect(filterEnvironmentLucideIconIds(" Cloud down ")).toEqual(["cloud-download"]); + expect(filterEnvironmentLucideIconIds("nothing-like-this")).toEqual([]); + }); +}); diff --git a/apps/web/src/components/settings/EnvironmentIconPickerDialog.tsx b/apps/web/src/components/settings/EnvironmentIconPickerDialog.tsx index e33a723d7c4d..47a37d26eb01 100644 --- a/apps/web/src/components/settings/EnvironmentIconPickerDialog.tsx +++ b/apps/web/src/components/settings/EnvironmentIconPickerDialog.tsx @@ -5,13 +5,15 @@ import { environmentIconForMachineKind, isEnvironmentCuratedIconId, hasLegacyEnvironmentIconForm, + isEnvironmentLucideIconId, type EnvironmentCuratedIconId, type EnvironmentIcon, + type EnvironmentLucideIconId, type EnvironmentMachineKind, type IconColor, type ServerConfig, } from "@t3tools/contracts"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { cn } from "~/lib/utils"; import { firstEmoji, PROJECT_EMOJIS } from "../../iconEmoji"; @@ -32,6 +34,7 @@ import { Input } from "../ui/input"; import { ScrollArea } from "../ui/scroll-area"; import { Toggle, ToggleGroup } from "../ui/toggle-group"; import { + filterEnvironmentLucideIconIds, resolveEnvironmentIconChoiceLock, resolveEnvironmentIconDialogWrite, resolveEnvironmentRichIconLock, @@ -40,6 +43,14 @@ import { const DEFAULT_EMOJI = "💻"; +/** A Lucide id as a label: `hard-drive` reads as `Hard Drive`. */ +function iconLabel(name: string): string { + return name + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + /** * The dialog's state for the current pick. Each mode starts from what is * stored, or from a sensible default. @@ -56,6 +67,8 @@ function initialState(input: { current.kind === "icon" && isEnvironmentCuratedIconId(current.name) ? current.name : input.detected, + lucideId: + current.kind === "icon" && isEnvironmentLucideIconId(current.name) ? current.name : null, color: current.kind === "icon" || current.kind === "monogram" ? (current.color ?? null) : null, emoji: current.kind === "emoji" ? current.emoji : DEFAULT_EMOJI, monogram: @@ -89,15 +102,19 @@ export function EnvironmentIconPickerDialog({ const initial = initialState({ current, detected, environmentLabel }); const [mode, setMode] = useState(initial.mode); const [iconId, setIconId] = useState(initial.iconId); + const [lucideId, setLucideId] = useState(initial.lucideId); + const [query, setQuery] = useState(""); const [color, setColor] = useState(initial.color); const [emoji, setEmoji] = useState(initial.emoji); const [monogram, setMonogram] = useState(initial.monogram); const [customEmoji, setCustomEmoji] = useState(""); const richLock = resolveEnvironmentRichIconLock(serverConfig); + const lucideIds = useMemo(() => filterEnvironmentLucideIconIds(query), [query]); const write = resolveEnvironmentIconDialogWrite({ mode, iconId, + lucideId, color, emoji, monogram, @@ -189,39 +206,86 @@ export function EnvironmentIconPickerDialog({ ) : null} {mode === "icon" ? ( -
- {ENVIRONMENT_CURATED_ICON_IDS.map((id) => { - const lock = resolveEnvironmentIconChoiceLock({ serverConfig, id }); - return ( - - ); - })} -
+ <> +
+ {ENVIRONMENT_CURATED_ICON_IDS.map((id) => { + const lock = resolveEnvironmentIconChoiceLock({ serverConfig, id }); + return ( + + ); + })} +
+ setQuery(event.currentTarget.value)} + /> + +
+ {lucideIds.map((id) => ( + + ))} +
+
+ {lucideIds.length === 0 ? ( +

No icons found.

+ ) : null} + ) : mode === "monogram" ? (