@cantileva/icons 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +40 -0
- package/runtime.d.ts +13 -0
- package/runtime.js +53 -0
- package/sync.js +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @cantileva/icons
|
|
2
|
+
|
|
3
|
+
Import **your** Cantileva icons as React components:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { TrashIcon } from "@cantileva/icons";
|
|
7
|
+
|
|
8
|
+
<TrashIcon className="size-6 text-amber-500" />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It works by **syncing** — a one-time download (per your API key) of the icons you
|
|
12
|
+
made in the Cantileva app into this package as real components. No AI, no bundler
|
|
13
|
+
plugin, no config: the bare import resolves in any bundler (Turbopack, Vite, webpack).
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i @cantileva/icons
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Set your API key (from the app → Settings → API keys). The sync reads it from your
|
|
22
|
+
environment or a `.env`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# .env
|
|
26
|
+
CANTILEVA_API_KEY=cnt_yourkey
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The icons sync automatically right after install. To re-sync after you add or edit
|
|
30
|
+
icons in the app, run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx cantileva
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then import any icon by name:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { TrashIcon, RocketShipIcon } from "@cantileva/icons";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Icons use `currentColor` and `1em` sizing, so they inherit text color and font-size.
|
|
43
|
+
Animated icons (ones you've animated in the app) also export a self-playing
|
|
44
|
+
component, e.g. `import { RocketShipAnimated } from "@cantileva/icons"`.
|
|
45
|
+
|
|
46
|
+
## How it works (and how it's secured)
|
|
47
|
+
|
|
48
|
+
`npm i` (and `npx cantileva`) run a **sync** that calls the Cantileva API with your
|
|
49
|
+
`CANTILEVA_API_KEY`. The backend **verifies the key and returns only your icons** —
|
|
50
|
+
no key, or an invalid one, and nothing is downloaded. The components are written
|
|
51
|
+
into the package, so importing them is a normal ESM import (no plugin, no runtime
|
|
52
|
+
fetch, key never ships to the browser).
|
|
53
|
+
|
|
54
|
+
This is the same shape as `prisma generate`: a local codegen step, scoped to you.
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- **No key yet?** Install still succeeds; the sync just skips. Set the key and run
|
|
59
|
+
`npx cantileva`.
|
|
60
|
+
- **CI:** set `CANTILEVA_API_KEY` as a secret; the postinstall sync picks it up. If
|
|
61
|
+
you run installs with `--ignore-scripts`, run `npx cantileva` as a build step.
|
|
62
|
+
- **Self-hosted / local:** point at your instance with `CANTILEVA_API_URL`
|
|
63
|
+
(default is the hosted Cantileva).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cantileva/icons",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Your Cantileva icon library, as React components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cantileva": "sync.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./runtime": {
|
|
17
|
+
"types": "./runtime.d.ts",
|
|
18
|
+
"default": "./runtime.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "node sync.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"sync.js",
|
|
28
|
+
"runtime.js",
|
|
29
|
+
"runtime.d.ts",
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"keywords": ["icons", "svg", "react", "cantileva"],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
package/runtime.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface IconProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
/** Icon slug, e.g. "trash" or "rocket-ship". */
|
|
5
|
+
name: string;
|
|
6
|
+
/** API key (defaults to NEXT_PUBLIC_CANTILEVA_API_KEY). */
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/** API base URL (defaults to NEXT_PUBLIC_CANTILEVA_API_URL or the hosted instance). */
|
|
9
|
+
url?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare function Icon(props: IconProps): ReactElement;
|
|
13
|
+
export default Icon;
|
package/runtime.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Optional runtime icon: <Icon name="trash" />. Fetches the SVG live from your
|
|
2
|
+
// Cantileva instance instead of syncing at build time — so new icons appear with
|
|
3
|
+
// no rebuild. Trade-offs vs the synced named imports: a network request per icon
|
|
4
|
+
// (cached), and the API key is used in the client, so use a read-only key you're
|
|
5
|
+
// OK exposing (or point `url` at your own same-origin proxy). Results are cached
|
|
6
|
+
// per icon name for the session.
|
|
7
|
+
|
|
8
|
+
import { createElement as h, useState, useEffect } from "react";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_URL = "https://app.cantileva.com/api";
|
|
11
|
+
const cache = new Map(); // name -> svg string
|
|
12
|
+
const inflight = new Map(); // name -> Promise<string>
|
|
13
|
+
|
|
14
|
+
function env(k) {
|
|
15
|
+
try { return typeof process !== "undefined" && process.env ? process.env[k] : undefined; } catch { return undefined; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function load(name, url, key) {
|
|
19
|
+
if (cache.has(name)) return Promise.resolve(cache.get(name));
|
|
20
|
+
if (inflight.has(name)) return inflight.get(name);
|
|
21
|
+
const p = fetch(`${url}/v1/icons/${encodeURIComponent(name)}`, { headers: key ? { authorization: `Bearer ${key}` } : {} })
|
|
22
|
+
.then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
|
23
|
+
.then((data) => {
|
|
24
|
+
const base = data.base || {};
|
|
25
|
+
const svg = base.solid || Object.values(base)[0] || "";
|
|
26
|
+
cache.set(name, svg);
|
|
27
|
+
inflight.delete(name);
|
|
28
|
+
return svg;
|
|
29
|
+
})
|
|
30
|
+
.catch((e) => { inflight.delete(name); throw e; });
|
|
31
|
+
inflight.set(name, p);
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Icon({ name, apiKey, url, ...props }) {
|
|
36
|
+
const base = (url || env("NEXT_PUBLIC_CANTILEVA_API_URL") || DEFAULT_URL).replace(/\/$/, "");
|
|
37
|
+
const key = apiKey || env("NEXT_PUBLIC_CANTILEVA_API_KEY") || "";
|
|
38
|
+
const [svg, setSvg] = useState(() => cache.get(name) || "");
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let live = true;
|
|
41
|
+
load(name, base, key).then((s) => { if (live) setSvg(s); }).catch(() => {});
|
|
42
|
+
return () => { live = false; };
|
|
43
|
+
}, [name, base, key]);
|
|
44
|
+
const html = svg
|
|
45
|
+
? svg.replace(/\swidth="[^"]*"/, "").replace(/\sheight="[^"]*"/, "").replace(/<svg/, '<svg width="100%" height="100%"')
|
|
46
|
+
: "";
|
|
47
|
+
return h("span", Object.assign({
|
|
48
|
+
style: { display: "inline-flex", width: "1em", height: "1em", color: "currentColor" },
|
|
49
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
50
|
+
}, props));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default Icon;
|
package/sync.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sync: download YOUR Cantileva icons (the ones you made in the web app) into
|
|
3
|
+
// this package as React components, so you can `import { FooIcon } from
|
|
4
|
+
// "@cantileva/icons"`. No icons are created here — this only fetches what's
|
|
5
|
+
// already in your library, gated by YOUR API key (the backend verifies it).
|
|
6
|
+
//
|
|
7
|
+
// Runs automatically on install (postinstall) and on demand via `npx cantileva`.
|
|
8
|
+
// It writes into this package's own dist/, so the bare import resolves in any
|
|
9
|
+
// bundler (Turbopack, Vite, webpack) with no plugin or config.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const SELF = path.dirname(fileURLToPath(import.meta.url)); // the installed package dir
|
|
16
|
+
const DIST = path.join(SELF, "dist");
|
|
17
|
+
const URL = (process.env.CANTILEVA_API_URL || "https://app.cantileva.com/api").replace(/\/$/, "");
|
|
18
|
+
|
|
19
|
+
// Pick up CANTILEVA_API_KEY from the consumer's .env if present (no hard dep).
|
|
20
|
+
try { process.loadEnvFile?.(); } catch { /* no .env */ }
|
|
21
|
+
const KEY = process.env.CANTILEVA_API_KEY;
|
|
22
|
+
|
|
23
|
+
const pascal = (s) => String(s).replace(/[^a-z0-9]+/gi, " ").trim().split(/\s+/).map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
24
|
+
const iconName = (slug) => pascal(slug) + "Icon";
|
|
25
|
+
const animName = (slug) => pascal(slug) + "Animated";
|
|
26
|
+
const viewBoxOf = (svg) => (svg.match(/viewBox="([^"]+)"/) || [, "0 0 1024 1024"])[1];
|
|
27
|
+
const innerOf = (svg) => svg.replace(/^[\s\S]*?<svg[^>]*>/, "").replace(/<\/svg>\s*$/, "").trim();
|
|
28
|
+
const fillHost = (svg) => svg.replace(/\swidth="[^"]*"/, ' width="100%"').replace(/\sheight="[^"]*"/, ' height="100%"');
|
|
29
|
+
|
|
30
|
+
const iconModule = (svg) => `import { createElement as h } from "react";
|
|
31
|
+
const __html = ${JSON.stringify(innerOf(svg))};
|
|
32
|
+
export default function Icon(props) {
|
|
33
|
+
return h("svg", Object.assign({ xmlns: "http://www.w3.org/2000/svg", viewBox: ${JSON.stringify(viewBoxOf(svg))}, width: "1em", height: "1em", fill: "currentColor", dangerouslySetInnerHTML: { __html } }, props));
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const animModule = (frames, fps) => `import { createElement as h, useState, useEffect } from "react";
|
|
37
|
+
const FRAMES = ${JSON.stringify(frames.map(fillHost))};
|
|
38
|
+
export default function Animated({ fps = ${fps}, ...props }) {
|
|
39
|
+
const [i, setI] = useState(0);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (FRAMES.length < 2) return;
|
|
42
|
+
const id = setInterval(() => setI((x) => (x + 1) % FRAMES.length), 1000 / fps);
|
|
43
|
+
return () => clearInterval(id);
|
|
44
|
+
}, [fps]);
|
|
45
|
+
return h("span", Object.assign({ style: { display: "inline-flex", width: "1em", height: "1em", color: "currentColor" }, dangerouslySetInnerHTML: { __html: FRAMES[i] } }, props));
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
const STUB_JS = `// Not synced yet. Set CANTILEVA_API_KEY and run \`npx cantileva\` to download your icons.\nexport {};\n`;
|
|
49
|
+
const STUB_DTS = `// Not synced yet — run \`npx cantileva\` (with CANTILEVA_API_KEY set).\nexport {};\n`;
|
|
50
|
+
|
|
51
|
+
function writeStub() {
|
|
52
|
+
fs.mkdirSync(DIST, { recursive: true });
|
|
53
|
+
fs.writeFileSync(path.join(DIST, "index.js"), STUB_JS);
|
|
54
|
+
fs.writeFileSync(path.join(DIST, "index.d.ts"), STUB_DTS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getJSON(route) {
|
|
58
|
+
const res = await fetch(URL + route, { headers: { authorization: `Bearer ${KEY}` } });
|
|
59
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${route}`);
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function sync() {
|
|
64
|
+
const { icons } = await getJSON("/v1/icons");
|
|
65
|
+
fs.rmSync(DIST, { recursive: true, force: true });
|
|
66
|
+
fs.mkdirSync(path.join(DIST, "icons"), { recursive: true });
|
|
67
|
+
const index = [];
|
|
68
|
+
const dts = [];
|
|
69
|
+
let n = 0;
|
|
70
|
+
let anims = 0;
|
|
71
|
+
for (const ic of icons || []) {
|
|
72
|
+
const base = ic.base || {};
|
|
73
|
+
const svg = base.solid || Object.values(base)[0];
|
|
74
|
+
if (!svg) continue;
|
|
75
|
+
const Name = iconName(ic.slug);
|
|
76
|
+
fs.writeFileSync(path.join(DIST, "icons", `${Name}.js`), iconModule(svg));
|
|
77
|
+
index.push(`export { default as ${Name} } from "./icons/${Name}.js";`);
|
|
78
|
+
dts.push(`export declare const ${Name}: FC<SVGProps<SVGSVGElement>>;`);
|
|
79
|
+
n++;
|
|
80
|
+
if (ic.hasAnimation) {
|
|
81
|
+
try {
|
|
82
|
+
const a = await getJSON(`/v1/icons/${encodeURIComponent(ic.slug)}/animation`);
|
|
83
|
+
if (a.frames?.length) {
|
|
84
|
+
const A = animName(ic.slug);
|
|
85
|
+
fs.writeFileSync(path.join(DIST, "icons", `${A}.js`), animModule(a.frames, a.fps || 12));
|
|
86
|
+
index.push(`export { default as ${A} } from "./icons/${A}.js";`);
|
|
87
|
+
dts.push(`export declare const ${A}: FC<HTMLAttributes<HTMLSpanElement> & { fps?: number }>;`);
|
|
88
|
+
anims++;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* skip this animation */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!n) throw new Error("no icons returned");
|
|
94
|
+
fs.writeFileSync(path.join(DIST, "index.js"), index.join("\n") + "\n");
|
|
95
|
+
fs.writeFileSync(path.join(DIST, "index.d.ts"), `import type { FC, SVGProps, HTMLAttributes } from "react";\n` + dts.join("\n") + "\n");
|
|
96
|
+
return { n, anims };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
|
|
100
|
+
|
|
101
|
+
(async () => {
|
|
102
|
+
if (!KEY) {
|
|
103
|
+
if (!fs.existsSync(path.join(DIST, "index.js"))) writeStub();
|
|
104
|
+
console.log("[@cantileva/icons] No CANTILEVA_API_KEY — skipped icon sync.\n Set it (env or .env), then run `npx cantileva`.");
|
|
105
|
+
process.exit(0); // never break an install
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const { n, anims } = await sync();
|
|
109
|
+
console.log(`[@cantileva/icons] Synced ${n} icons${anims ? ` + ${anims} animations` : ""}.`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (!fs.existsSync(path.join(DIST, "index.js"))) writeStub();
|
|
113
|
+
console.warn(`[@cantileva/icons] Sync failed: ${e.message}` + (isPostinstall ? "\n Run `npx cantileva` once your key/connection is ready." : ""));
|
|
114
|
+
process.exit(isPostinstall ? 0 : 1); // don't fail installs; do fail explicit runs
|
|
115
|
+
}
|
|
116
|
+
})();
|