@stateglyph/cli 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamalveer Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @stateglyph/cli
2
+
3
+ Copy editable StateGlyph components into a React project.
4
+
5
+ ```bash
6
+ npx @stateglyph/cli list
7
+ npx @stateglyph/cli add upload
8
+ ```
9
+
10
+ Add more than one icon or choose a different destination:
11
+
12
+ ```bash
13
+ npx @stateglyph/cli add upload save --dir src/components/stateglyph
14
+ npx @stateglyph/cli add all
15
+ ```
16
+
17
+ Use `--dry-run` to preview files and `--force` to replace existing files.
18
+ Without `--force`, the CLI safely skips files that already exist.
19
+
20
+ The generated components are TypeScript React files. They are self-contained
21
+ and depend only on `react` and `lucide-react`, so you can edit them freely in
22
+ your own project.
23
+
24
+ MIT licensed. The source icons from Lucide use the ISC License.
@@ -0,0 +1,52 @@
1
+ # Third-party notices
2
+
3
+ StateGlyph uses glyphs from [Lucide](https://lucide.dev/) through
4
+ `lucide-react`. Lucide is licensed under the ISC License. Some Lucide glyphs
5
+ are derived from Feather Icons and remain available under the MIT License.
6
+
7
+ ## Lucide Icons
8
+
9
+ ISC License
10
+
11
+ Copyright (c) 2026 Lucide Icons and Contributors
12
+
13
+ Permission to use, copy, modify, and/or distribute this software for any
14
+ purpose with or without fee is hereby granted, provided that the above
15
+ copyright notice and this permission notice appear in all copies.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
18
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
19
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
21
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
22
+ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
23
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
24
+
25
+ ## Feather Icons
26
+
27
+ The following Lucide glyphs used by StateGlyph are derived from Feather Icons:
28
+ `check`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`,
29
+ `download`, `maximize`, `minimize`, `minimize-2`, `pause`, `sidebar`,
30
+ `trash-2`, `upload`, and `x`.
31
+
32
+ The MIT License (MIT)
33
+
34
+ Copyright (c) 2013-present Cole Bemis
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy
37
+ of this software and associated documentation files (the "Software"), to deal
38
+ in the Software without restriction, including without limitation the rights
39
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40
+ copies of the Software, and to permit persons to whom the Software is
41
+ furnished to do so, subject to the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be included in all
44
+ copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/add.ts
7
+ import { access, mkdir, writeFile } from "fs/promises";
8
+ import path from "path";
9
+ import { stateIconCatalog } from "@stateglyph/core";
10
+
11
+ // src/template.ts
12
+ function toPascalCase(value) {
13
+ return value.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
14
+ }
15
+ function renderIconComponent(definition) {
16
+ const componentName = `${toPascalCase(definition.id)}StateIcon`;
17
+ const stateTypeName = `${toPascalCase(definition.id)}State`;
18
+ const propsTypeName = `${componentName}Props`;
19
+ const states = Object.entries(definition.states);
20
+ const lucideComponents = [
21
+ ...new Set(states.map(([, stateDefinition]) => stateDefinition.icon))
22
+ ].map(toPascalCase).sort();
23
+ const continuousStates = states.filter(([, stateDefinition]) => stateDefinition.continuous).map(([name]) => name);
24
+ const animationName = `stateglyph-${definition.id}-spin`;
25
+ const stateUnion = states.map(([name]) => JSON.stringify(name)).join(" | ");
26
+ const iconRows = states.map(
27
+ ([name, stateDefinition]) => ` ${JSON.stringify(name)}: ${toPascalCase(stateDefinition.icon)},`
28
+ ).join("\n");
29
+ const labelRows = states.map(
30
+ ([name, stateDefinition]) => ` ${JSON.stringify(name)}: ${JSON.stringify(stateDefinition.label)},`
31
+ ).join("\n");
32
+ return `import type { SVGProps } from "react";
33
+ import {
34
+ ${lucideComponents.map((name) => ` ${name},`).join("\n")}
35
+ type LucideIcon,
36
+ } from "lucide-react";
37
+
38
+ export type ${stateTypeName} = ${stateUnion};
39
+
40
+ export type ${propsTypeName} = Omit<SVGProps<SVGSVGElement>, "children"> & {
41
+ state: ${stateTypeName};
42
+ size?: number | string;
43
+ strokeWidth?: number;
44
+ duration?: number;
45
+ decorative?: boolean;
46
+ label?: string;
47
+ };
48
+
49
+ const iconByState = {
50
+ ${iconRows}
51
+ } satisfies Record<${stateTypeName}, LucideIcon>;
52
+
53
+ const labelByState = {
54
+ ${labelRows}
55
+ } satisfies Record<${stateTypeName}, string>;
56
+
57
+ const continuousStates = new Set<${stateTypeName}>(${JSON.stringify(continuousStates)});
58
+
59
+ export function ${componentName}({
60
+ state,
61
+ size = 24,
62
+ strokeWidth = 2,
63
+ duration = 900,
64
+ decorative = true,
65
+ label,
66
+ style,
67
+ ...svgProps
68
+ }: ${propsTypeName}) {
69
+ const Icon = iconByState[state];
70
+ const isContinuous = continuousStates.has(state);
71
+ const accessibleLabel = label ?? labelByState[state];
72
+
73
+ return (
74
+ <>
75
+ {isContinuous ? (
76
+ <style>{\`@keyframes ${animationName} { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { [data-state-icon="${definition.id}"] { animation: none !important; } }\`}</style>
77
+ ) : null}
78
+ <Icon
79
+ {...svgProps}
80
+ size={size}
81
+ strokeWidth={strokeWidth}
82
+ data-state-icon="${definition.id}"
83
+ data-state={state}
84
+ aria-hidden={decorative ? true : undefined}
85
+ aria-label={decorative ? undefined : accessibleLabel}
86
+ role={decorative ? undefined : "img"}
87
+ focusable="false"
88
+ style={{
89
+ ...style,
90
+ animation:
91
+ style?.animation ??
92
+ (isContinuous
93
+ ? \`${animationName} \${duration}ms linear infinite\`
94
+ : undefined),
95
+ }}
96
+ />
97
+ </>
98
+ );
99
+ }
100
+ `;
101
+ }
102
+
103
+ // src/add.ts
104
+ async function fileExists(filePath) {
105
+ try {
106
+ await access(filePath);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+ async function addIcons(requestedIcons, options = {}) {
113
+ const cwd = options.cwd ?? process.cwd();
114
+ const directory = options.directory ?? "components/stateglyph";
115
+ const destination = path.resolve(cwd, directory);
116
+ const requestedNames = requestedIcons.includes("all") ? stateIconCatalog.map((icon) => icon.id) : [...new Set(requestedIcons)];
117
+ const unknownNames = requestedNames.filter(
118
+ (name) => !stateIconCatalog.some((icon) => icon.id === name)
119
+ );
120
+ if (unknownNames.length > 0) {
121
+ throw new Error(
122
+ `Unknown icon${unknownNames.length === 1 ? "" : "s"}: ${unknownNames.join(", ")}. Run 'stateglyph list' to see valid names.`
123
+ );
124
+ }
125
+ const result = { added: [], skipped: [] };
126
+ for (const name of requestedNames) {
127
+ const icon = stateIconCatalog.find((item) => item.id === name);
128
+ if (!icon) {
129
+ continue;
130
+ }
131
+ const relativePath = path.join(directory, `${icon.id}-state-icon.tsx`);
132
+ const filePath = path.resolve(cwd, relativePath);
133
+ if (await fileExists(filePath) && !options.force) {
134
+ result.skipped.push(relativePath);
135
+ continue;
136
+ }
137
+ result.added.push(relativePath);
138
+ if (!options.dryRun) {
139
+ await mkdir(destination, { recursive: true });
140
+ await writeFile(filePath, renderIconComponent(icon), "utf8");
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+
146
+ // src/list.ts
147
+ import { stateIconCatalog as stateIconCatalog2 } from "@stateglyph/core";
148
+ var categoryLabels = {
149
+ async: "Async workflows",
150
+ media: "Media controls",
151
+ navigation: "Navigation and layout",
152
+ feedback: "Feedback and toggles",
153
+ device: "Device and connection",
154
+ form: "Form states",
155
+ commerce: "Commerce",
156
+ notification: "Notifications"
157
+ };
158
+ function formatIconList() {
159
+ const sections = Object.entries(categoryLabels).flatMap(
160
+ ([category, label]) => {
161
+ const icons = stateIconCatalog2.filter(
162
+ (icon) => icon.category === category
163
+ );
164
+ if (icons.length === 0) {
165
+ return [];
166
+ }
167
+ const rows = icons.map(
168
+ (icon) => ` ${icon.id.padEnd(22)} ${icon.title} (${Object.keys(icon.states).length} states)`
169
+ );
170
+ return [[`${label} (${icons.length})`, ...rows].join("\n")];
171
+ }
172
+ );
173
+ return [
174
+ `StateGlyph \u2014 ${stateIconCatalog2.length} icons`,
175
+ "",
176
+ ...sections.flatMap(
177
+ (section, index) => index === sections.length - 1 ? [section] : [section, ""]
178
+ )
179
+ ].join("\n");
180
+ }
181
+
182
+ // src/index.ts
183
+ var program = new Command();
184
+ program.name("stateglyph").description("Browse and add StateGlyph components to a React project.").version("0.1.0").showHelpAfterError();
185
+ program.command("list").description("List every available icon, grouped by category.").action(() => {
186
+ process.stdout.write(`${formatIconList()}
187
+ `);
188
+ });
189
+ program.command("add").description(
190
+ "Copy one or more StateGlyph components into the current project."
191
+ ).argument("<icons...>", "Icon names to add, or 'all'").option(
192
+ "-d, --dir <directory>",
193
+ "Destination directory",
194
+ "components/stateglyph"
195
+ ).option("--force", "Overwrite existing icon files", false).option(
196
+ "--dry-run",
197
+ "Show what would be created without writing files",
198
+ false
199
+ ).action(
200
+ async (icons, options) => {
201
+ try {
202
+ const result = await addIcons(icons, {
203
+ directory: options.dir,
204
+ force: options.force,
205
+ dryRun: options.dryRun
206
+ });
207
+ const verb = options.dryRun ? "Would add" : "Added";
208
+ if (result.added.length > 0) {
209
+ process.stdout.write(`${verb}:
210
+ `);
211
+ for (const file of result.added) {
212
+ process.stdout.write(` ${file}
213
+ `);
214
+ }
215
+ }
216
+ if (result.skipped.length > 0) {
217
+ process.stdout.write("Skipped existing files:\n");
218
+ for (const file of result.skipped) {
219
+ process.stdout.write(` ${file}
220
+ `);
221
+ }
222
+ process.stdout.write("Use --force to overwrite them.\n");
223
+ }
224
+ if (!options.dryRun && result.added.length > 0) {
225
+ process.stdout.write(
226
+ "\nThe generated components require react and lucide-react.\n"
227
+ );
228
+ }
229
+ } catch (error) {
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ program.error(message);
232
+ }
233
+ }
234
+ );
235
+ program.action(() => {
236
+ program.outputHelp();
237
+ });
238
+ await program.parseAsync();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@stateglyph/cli",
3
+ "version": "0.1.0",
4
+ "description": "Copy StateGlyph components into React projects.",
5
+ "license": "MIT",
6
+ "author": "Kamalveer Singh",
7
+ "keywords": [
8
+ "cli",
9
+ "react",
10
+ "icons",
11
+ "typescript",
12
+ "codegen"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/kamalhara/StateGlyph.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "homepage": "https://github.com/kamalhara/StateGlyph#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/kamalhara/StateGlyph/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "bin": {
28
+ "stateglyph": "./dist/index.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE",
34
+ "THIRD_PARTY_NOTICES.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm --dts --clean",
38
+ "dev": "tsup src/index.ts --format esm --dts --watch",
39
+ "prepack": "node ../../scripts/sync-package-legal-files.mjs",
40
+ "typecheck": "tsc --noEmit",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "dependencies": {
44
+ "@stateglyph/core": "0.1.0",
45
+ "commander": "^15.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=22.19.0"
49
+ }
50
+ }