stera-ui 0.1.2 → 0.2.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.
@@ -34,7 +34,11 @@ var configSchema = z.object({
34
34
  ui: z.string(),
35
35
  lib: z.string(),
36
36
  hooks: z.string()
37
- })
37
+ }),
38
+ fonts: z.object({
39
+ strategy: z.enum(["stera-default", "keep-existing", "skip"]),
40
+ nextFont: z.boolean().optional()
41
+ }).optional()
38
42
  });
39
43
 
40
44
  export {
@@ -165,7 +165,22 @@ function detectProject(cwd) {
165
165
  rsc = fs2.existsSync(path3.join(cwd, appDir));
166
166
  }
167
167
  const existingCssFile = findTailwindCssFile(cwd);
168
- return { hasSrc, framework, rsc, existingCssFile };
168
+ let layoutFile = null;
169
+ if (framework === "next") {
170
+ const appDir = hasSrc ? "src/app" : "app";
171
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
172
+ const candidate = `${appDir}/layout.${ext}`;
173
+ if (fs2.existsSync(path3.join(cwd, candidate))) {
174
+ layoutFile = candidate;
175
+ break;
176
+ }
177
+ }
178
+ } else if (framework === "vite") {
179
+ if (fs2.existsSync(path3.join(cwd, "index.html"))) {
180
+ layoutFile = "index.html";
181
+ }
182
+ }
183
+ return { hasSrc, framework, rsc, existingCssFile, layoutFile };
169
184
  }
170
185
 
171
186
  // src/utils/detect-globals.ts
@@ -183,7 +198,8 @@ function hasGlobalsCss(config, projectRoot) {
183
198
  // src/utils/install-deps.ts
184
199
  import fs4 from "fs";
185
200
  import path5 from "path";
186
- import { spawn } from "child_process";
201
+ import { detect } from "@antfu/ni";
202
+ import { execa } from "execa";
187
203
 
188
204
  // src/utils/format.ts
189
205
  var BRAND = "\x1B[38;2;166;244;48m";
@@ -241,10 +257,16 @@ function createSpinner(text) {
241
257
  }
242
258
 
243
259
  // src/utils/install-deps.ts
244
- function detectPackageManager(cwd) {
245
- if (fs4.existsSync(path5.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
246
- if (fs4.existsSync(path5.join(cwd, "bun.lockb"))) return "bun";
247
- if (fs4.existsSync(path5.join(cwd, "yarn.lock"))) return "yarn";
260
+ async function detectPackageManager(cwd) {
261
+ const detected = await detect({ programmatic: true, cwd });
262
+ if (detected === "pnpm@6" || detected === "pnpm") return "pnpm";
263
+ if (detected === "yarn@berry" || detected === "yarn") return "yarn";
264
+ if (detected === "bun") return "bun";
265
+ if (detected === "npm") return "npm";
266
+ const userAgent = process.env.npm_config_user_agent || "";
267
+ if (userAgent.startsWith("pnpm")) return "pnpm";
268
+ if (userAgent.startsWith("yarn")) return "yarn";
269
+ if (userAgent.startsWith("bun")) return "bun";
248
270
  return "npm";
249
271
  }
250
272
  function getInstallArgs(pm, deps) {
@@ -272,25 +294,16 @@ async function installDependencies(deps, cwd) {
272
294
  }
273
295
  const toInstall = deps.filter((dep) => !existing.has(dep));
274
296
  if (toInstall.length === 0) return;
275
- const pm = detectPackageManager(cwd);
297
+ const pm = await detectPackageManager(cwd);
276
298
  const [cmd, args] = getInstallArgs(pm, toInstall);
277
299
  const spinner = createSpinner(`Installing with ${pm}`);
278
- await new Promise((resolve, reject) => {
279
- const proc = spawn(cmd, args, { cwd, stdio: "pipe" });
280
- proc.on("close", (code) => {
281
- if (code === 0) {
282
- spinner.succeed(`Installed with ${pm}`);
283
- resolve();
284
- } else {
285
- spinner.fail(`Install failed (exit ${code})`);
286
- reject(new Error(`${pm} exited with code ${code}`));
287
- }
288
- });
289
- proc.on("error", (err) => {
290
- spinner.fail(`Install failed`);
291
- reject(err);
292
- });
293
- });
300
+ try {
301
+ await execa(cmd, args, { cwd });
302
+ spinner.succeed(`Installed with ${pm}`);
303
+ } catch (error) {
304
+ spinner.fail(`Install failed`);
305
+ throw error;
306
+ }
294
307
  }
295
308
 
296
309
  // src/utils/write-files.ts
package/dist/cli.js CHANGED
@@ -182,7 +182,8 @@ async function writeComponentFiles(items, config, projectRoot, options = {}) {
182
182
  // src/utils/install-deps.ts
183
183
  import fs3 from "fs";
184
184
  import path4 from "path";
185
- import { spawn } from "child_process";
185
+ import { detect } from "@antfu/ni";
186
+ import { execa } from "execa";
186
187
 
187
188
  // src/utils/format.ts
188
189
  var BRAND = "\x1B[38;2;166;244;48m";
@@ -240,10 +241,16 @@ function createSpinner(text) {
240
241
  }
241
242
 
242
243
  // src/utils/install-deps.ts
243
- function detectPackageManager(cwd) {
244
- if (fs3.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
245
- if (fs3.existsSync(path4.join(cwd, "bun.lockb"))) return "bun";
246
- if (fs3.existsSync(path4.join(cwd, "yarn.lock"))) return "yarn";
244
+ async function detectPackageManager(cwd) {
245
+ const detected = await detect({ programmatic: true, cwd });
246
+ if (detected === "pnpm@6" || detected === "pnpm") return "pnpm";
247
+ if (detected === "yarn@berry" || detected === "yarn") return "yarn";
248
+ if (detected === "bun") return "bun";
249
+ if (detected === "npm") return "npm";
250
+ const userAgent = process.env.npm_config_user_agent || "";
251
+ if (userAgent.startsWith("pnpm")) return "pnpm";
252
+ if (userAgent.startsWith("yarn")) return "yarn";
253
+ if (userAgent.startsWith("bun")) return "bun";
247
254
  return "npm";
248
255
  }
249
256
  function getInstallArgs(pm, deps) {
@@ -271,25 +278,16 @@ async function installDependencies(deps, cwd) {
271
278
  }
272
279
  const toInstall = deps.filter((dep) => !existing.has(dep));
273
280
  if (toInstall.length === 0) return;
274
- const pm = detectPackageManager(cwd);
281
+ const pm = await detectPackageManager(cwd);
275
282
  const [cmd, args] = getInstallArgs(pm, toInstall);
276
283
  const spinner = createSpinner(`Installing with ${pm}`);
277
- await new Promise((resolve, reject) => {
278
- const proc = spawn(cmd, args, { cwd, stdio: "pipe" });
279
- proc.on("close", (code) => {
280
- if (code === 0) {
281
- spinner.succeed(`Installed with ${pm}`);
282
- resolve();
283
- } else {
284
- spinner.fail(`Install failed (exit ${code})`);
285
- reject(new Error(`${pm} exited with code ${code}`));
286
- }
287
- });
288
- proc.on("error", (err) => {
289
- spinner.fail(`Install failed`);
290
- reject(err);
291
- });
292
- });
284
+ try {
285
+ await execa(cmd, args, { cwd });
286
+ spinner.succeed(`Installed with ${pm}`);
287
+ } catch (error) {
288
+ spinner.fail(`Install failed`);
289
+ throw error;
290
+ }
293
291
  }
294
292
 
295
293
  // src/utils/transform.ts
@@ -446,8 +444,9 @@ async function add(components, options) {
446
444
  }
447
445
 
448
446
  // src/commands/init.ts
449
- import fs6 from "fs";
450
- import path8 from "path";
447
+ import fs11 from "fs";
448
+ import path12 from "path";
449
+ import { select, confirm } from "@inquirer/prompts";
451
450
 
452
451
  // src/utils/detect-project.ts
453
452
  import fs5 from "fs";
@@ -488,11 +487,375 @@ function detectProject(cwd) {
488
487
  rsc = fs5.existsSync(path7.join(cwd, appDir));
489
488
  }
490
489
  const existingCssFile = findTailwindCssFile(cwd);
491
- return { hasSrc, framework, rsc, existingCssFile };
490
+ let layoutFile = null;
491
+ if (framework === "next") {
492
+ const appDir = hasSrc ? "src/app" : "app";
493
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
494
+ const candidate = `${appDir}/layout.${ext}`;
495
+ if (fs5.existsSync(path7.join(cwd, candidate))) {
496
+ layoutFile = candidate;
497
+ break;
498
+ }
499
+ }
500
+ } else if (framework === "vite") {
501
+ if (fs5.existsSync(path7.join(cwd, "index.html"))) {
502
+ layoutFile = "index.html";
503
+ }
504
+ }
505
+ return { hasSrc, framework, rsc, existingCssFile, layoutFile };
506
+ }
507
+
508
+ // src/utils/detect-fonts.ts
509
+ import fs6 from "fs";
510
+ import path8 from "path";
511
+ function readIfExists(filePath) {
512
+ try {
513
+ return fs6.readFileSync(filePath, "utf-8");
514
+ } catch {
515
+ return null;
516
+ }
517
+ }
518
+ function detectNextFont(layoutContent) {
519
+ const hasNextFont = /import.*from\s+['"]next\/font/.test(layoutContent) || /import.*from\s+['"]@next\/font/.test(layoutContent) || /import.*from\s+['"]geist\/font/.test(layoutContent);
520
+ let nextFontVariable = null;
521
+ const varMatch = layoutContent.match(/variable:\s*['"](--.+?)['"]/);
522
+ if (varMatch) {
523
+ nextFontVariable = varMatch[1];
524
+ }
525
+ return { hasNextFont, nextFontVariable };
526
+ }
527
+ function detectFontFace(cssContent) {
528
+ const hasFontFace = /@font-face\s*\{/.test(cssContent);
529
+ const fontFamilies = [];
530
+ const regex = /font-family:\s*['"]?([^'";}\n]+?)['"]?\s*;/g;
531
+ let match;
532
+ while ((match = regex.exec(cssContent)) !== null) {
533
+ const family = match[1].trim();
534
+ if (family && !fontFamilies.includes(family)) {
535
+ fontFamilies.push(family);
536
+ }
537
+ }
538
+ return { hasFontFace, fontFamilies };
539
+ }
540
+ function detectGoogleFonts(content) {
541
+ return content.includes("fonts.googleapis.com") || content.includes("fonts.gstatic.com");
542
+ }
543
+ function detectFonts(cwd, project) {
544
+ const result = {
545
+ hasNextFont: false,
546
+ hasFontFace: false,
547
+ hasGoogleFontsLink: false,
548
+ fontFamilies: [],
549
+ nextFontVariable: null
550
+ };
551
+ if (project.layoutFile) {
552
+ const layoutContent = readIfExists(path8.join(cwd, project.layoutFile));
553
+ if (layoutContent) {
554
+ const nextFontResult = detectNextFont(layoutContent);
555
+ result.hasNextFont = nextFontResult.hasNextFont;
556
+ result.nextFontVariable = nextFontResult.nextFontVariable;
557
+ if (detectGoogleFonts(layoutContent)) {
558
+ result.hasGoogleFontsLink = true;
559
+ }
560
+ }
561
+ }
562
+ if (project.existingCssFile) {
563
+ const cssContent = readIfExists(path8.join(cwd, project.existingCssFile));
564
+ if (cssContent) {
565
+ const fontFaceResult = detectFontFace(cssContent);
566
+ result.hasFontFace = fontFaceResult.hasFontFace;
567
+ result.fontFamilies.push(...fontFaceResult.fontFamilies);
568
+ if (detectGoogleFonts(cssContent)) {
569
+ result.hasGoogleFontsLink = true;
570
+ }
571
+ const importRegex = /@import\s+['"](.+?)['"]/g;
572
+ let importMatch;
573
+ while ((importMatch = importRegex.exec(cssContent)) !== null) {
574
+ const importPath = importMatch[1];
575
+ const resolvedPath = path8.join(
576
+ cwd,
577
+ path8.dirname(project.existingCssFile),
578
+ importPath
579
+ );
580
+ const importedContent = readIfExists(resolvedPath);
581
+ if (importedContent) {
582
+ const imported = detectFontFace(importedContent);
583
+ if (imported.hasFontFace) result.hasFontFace = true;
584
+ for (const family of imported.fontFamilies) {
585
+ if (!result.fontFamilies.includes(family)) {
586
+ result.fontFamilies.push(family);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ for (const htmlFile of ["index.html", "src/index.html"]) {
594
+ const htmlContent = readIfExists(path8.join(cwd, htmlFile));
595
+ if (htmlContent && detectGoogleFonts(htmlContent)) {
596
+ result.hasGoogleFontsLink = true;
597
+ }
598
+ }
599
+ return result;
600
+ }
601
+
602
+ // src/utils/patch-css-vars.ts
603
+ import fs7 from "fs";
604
+ function patchCssVariables(filePath, patches) {
605
+ const content = fs7.readFileSync(filePath, "utf-8");
606
+ const lines = content.split("\n");
607
+ const patched = [];
608
+ for (let i = 0; i < lines.length; i++) {
609
+ for (const [prop, newValue] of Object.entries(patches)) {
610
+ const regex = new RegExp(`(\\s*${escapeRegex(prop)}:\\s*).+?(;)`);
611
+ if (regex.test(lines[i])) {
612
+ lines[i] = lines[i].replace(regex, `$1${newValue}$2`);
613
+ if (!patched.includes(prop)) patched.push(prop);
614
+ }
615
+ }
616
+ }
617
+ if (patched.length > 0) {
618
+ fs7.writeFileSync(filePath, lines.join("\n"), "utf-8");
619
+ }
620
+ return patched;
621
+ }
622
+ function removeCssImport(filePath, importPath) {
623
+ const content = fs7.readFileSync(filePath, "utf-8");
624
+ const pattern = new RegExp(
625
+ `@import\\s+['"]${escapeRegex(importPath)}['"]\\s*;?\\n?`
626
+ );
627
+ if (!pattern.test(content)) return false;
628
+ fs7.writeFileSync(filePath, content.replace(pattern, ""), "utf-8");
629
+ return true;
630
+ }
631
+ function escapeRegex(str) {
632
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
633
+ }
634
+
635
+ // src/utils/setup-fonts-generic.ts
636
+ import fs9 from "fs";
637
+ import path10 from "path";
638
+
639
+ // src/utils/copy-fonts.ts
640
+ import fs8 from "fs";
641
+ import path9 from "path";
642
+ import { createRequire } from "module";
643
+ var FONT_FILES = [
644
+ {
645
+ src: "geist-sans/Geist-Variable.woff2",
646
+ dest: "geist/Geist-Variable.woff2"
647
+ },
648
+ {
649
+ src: "geist-sans/Geist-Italic[wght].woff2",
650
+ dest: "geist/Geist-Italic.woff2"
651
+ },
652
+ {
653
+ src: "geist-mono/GeistMono-Variable.woff2",
654
+ dest: "geist-mono/GeistMono-Variable.woff2"
655
+ },
656
+ {
657
+ src: "geist-mono/GeistMono-Italic[wght].woff2",
658
+ dest: "geist-mono/GeistMono-Italic.woff2"
659
+ }
660
+ ];
661
+ function resolveGeistFontsDir(cwd) {
662
+ const require2 = createRequire(path9.join(cwd, "package.json"));
663
+ const geistEntry = require2.resolve("geist/font/sans");
664
+ const distDir = path9.dirname(geistEntry);
665
+ return path9.join(distDir, "fonts");
666
+ }
667
+ function copyFonts(cwd) {
668
+ const spinner = createSpinner("Copying font files");
669
+ const fontsDir = resolveGeistFontsDir(cwd);
670
+ const publicFontsDir = path9.join(cwd, "public", "fonts");
671
+ const written = [];
672
+ for (const file of FONT_FILES) {
673
+ const srcPath = path9.join(fontsDir, file.src);
674
+ const destPath = path9.join(publicFontsDir, file.dest);
675
+ if (fs8.existsSync(destPath)) continue;
676
+ if (!fs8.existsSync(srcPath)) {
677
+ spinner.fail(`Font file not found: ${file.src}`);
678
+ throw new Error(
679
+ `Could not find ${srcPath}. Is the geist package installed?`
680
+ );
681
+ }
682
+ fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
683
+ fs8.copyFileSync(srcPath, destPath);
684
+ written.push(path9.relative(cwd, destPath));
685
+ }
686
+ if (written.length > 0) {
687
+ spinner.succeed(`Copied ${written.length} font files to public/fonts/`);
688
+ } else {
689
+ spinner.succeed("Font files already exist");
690
+ }
691
+ return written;
692
+ }
693
+
694
+ // src/utils/setup-fonts-generic.ts
695
+ var FONTS_CSS_CONTENT = `/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
696
+ Stera UI font registry
697
+ To add a font:
698
+ 1. Drop .woff2 file(s) into public/fonts/{family-name}/
699
+ 2. Add an @font-face block below
700
+ 3. Add a --font-* variable in globals.css
701
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
702
+
703
+ /* \u2500\u2500\u2500 Geist Sans \u2500\u2500\u2500 */
704
+ @font-face {
705
+ font-family: 'Geist';
706
+ src: url('/fonts/geist/Geist-Variable.woff2') format('woff2');
707
+ font-weight: 100 900;
708
+ font-style: normal;
709
+ font-display: swap;
710
+ }
711
+
712
+ @font-face {
713
+ font-family: 'Geist';
714
+ src: url('/fonts/geist/Geist-Italic.woff2') format('woff2');
715
+ font-weight: 100 900;
716
+ font-style: italic;
717
+ font-display: swap;
718
+ }
719
+
720
+ /* \u2500\u2500\u2500 Geist Mono \u2500\u2500\u2500 */
721
+ @font-face {
722
+ font-family: 'Geist Mono';
723
+ src: url('/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2');
724
+ font-weight: 100 900;
725
+ font-style: normal;
726
+ font-display: swap;
727
+ }
728
+
729
+ @font-face {
730
+ font-family: 'Geist Mono';
731
+ src: url('/fonts/geist-mono/GeistMono-Italic.woff2') format('woff2');
732
+ font-weight: 100 900;
733
+ font-style: italic;
734
+ font-display: swap;
735
+ }
736
+ `;
737
+ var PRELOAD_TAGS = `<link rel="preload" href="/fonts/geist/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
738
+ <link rel="preload" href="/fonts/geist-mono/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin />`;
739
+ function setupFontsGeneric(cwd, cssDir) {
740
+ copyFonts(cwd);
741
+ const fontsPath = path10.join(cssDir, "fonts.css");
742
+ if (!fs9.existsSync(fontsPath)) {
743
+ fs9.mkdirSync(path10.dirname(fontsPath), { recursive: true });
744
+ fs9.writeFileSync(fontsPath, FONTS_CSS_CONTENT, "utf-8");
745
+ console.log(` ${CHECK} Created ${path10.relative(cwd, fontsPath)}`);
746
+ } else {
747
+ console.log(` ${dim(path10.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
748
+ }
749
+ return { preloadTags: PRELOAD_TAGS };
750
+ }
751
+
752
+ // src/utils/setup-fonts-nextjs.ts
753
+ import fs10 from "fs";
754
+ import path11 from "path";
755
+ var GEIST_IMPORTS = `import { GeistSans } from "geist/font/sans"
756
+ import { GeistMono } from "geist/font/mono"`;
757
+ function canPatchLayout(content) {
758
+ if (!/<html[\s>]/.test(content)) return false;
759
+ if ((content.match(/<html[\s>]/g) || []).length > 1) return false;
760
+ return true;
761
+ }
762
+ function insertImports(content, imports) {
763
+ const lines = content.split("\n");
764
+ let lastImportIndex = -1;
765
+ for (let i = 0; i < lines.length; i++) {
766
+ const trimmed = lines[i].trim();
767
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import ")) {
768
+ lastImportIndex = i;
769
+ }
770
+ }
771
+ if (lastImportIndex >= 0) {
772
+ lines.splice(lastImportIndex + 1, 0, imports);
773
+ } else {
774
+ lines.unshift(imports);
775
+ }
776
+ return lines.join("\n");
777
+ }
778
+ function addClassNameToHtml(content) {
779
+ const fontClasses = "${GeistSans.variable} ${GeistMono.variable}";
780
+ const templateMatch = content.match(
781
+ /(<html[^>]*className=\{`)(.*?)(`\})/s
782
+ );
783
+ if (templateMatch) {
784
+ const existing = templateMatch[2].trim();
785
+ const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
786
+ return content.replace(
787
+ templateMatch[0],
788
+ `${templateMatch[1]}${newClassName}${templateMatch[3]}`
789
+ );
790
+ }
791
+ const exprMatch = content.match(
792
+ /(<html[^>]*className=\{)([^}`]+)(\})/
793
+ );
794
+ if (exprMatch) {
795
+ return content.replace(
796
+ exprMatch[0],
797
+ `${exprMatch[1]}\`\${${exprMatch[2].trim()}} ${fontClasses}\`${exprMatch[3]}`
798
+ );
799
+ }
800
+ const stringMatch = content.match(
801
+ /(<html[^>]*className=)(["'])([^"']*)(["'])/
802
+ );
803
+ if (stringMatch) {
804
+ const existing = stringMatch[3].trim();
805
+ const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
806
+ return content.replace(
807
+ stringMatch[0],
808
+ `${stringMatch[1]}{\`${newClassName}\`}`
809
+ );
810
+ }
811
+ return content.replace(
812
+ /<html(\s)/,
813
+ `<html className={\`${fontClasses}\`}$1`
814
+ );
815
+ }
816
+ function generateSnippet() {
817
+ return `
818
+ Add these imports to your root layout file:
819
+
820
+ ${dim(GEIST_IMPORTS)}
821
+
822
+ Then add the font variables to your <html> tag:
823
+
824
+ ${dim("<html className={`${GeistSans.variable} ${GeistMono.variable}`}>")}
825
+ `;
826
+ }
827
+ function setupFontsNextjs(cwd, layoutFile, cssFile) {
828
+ const layoutPath = path11.join(cwd, layoutFile);
829
+ const cssPath = path11.join(cwd, cssFile);
830
+ let layoutContent = fs10.readFileSync(layoutPath, "utf-8");
831
+ if (layoutContent.includes("geist/font/sans") || layoutContent.includes("GeistSans")) {
832
+ console.log(` ${dim("geist/font already configured in " + layoutFile)}`);
833
+ return true;
834
+ }
835
+ if (!canPatchLayout(layoutContent)) {
836
+ console.log(` ${WARN} Could not auto-patch ${layoutFile}`);
837
+ console.log(generateSnippet());
838
+ return false;
839
+ }
840
+ layoutContent = insertImports(layoutContent, GEIST_IMPORTS);
841
+ layoutContent = addClassNameToHtml(layoutContent);
842
+ fs10.writeFileSync(layoutPath, layoutContent, "utf-8");
843
+ console.log(` ${CHECK} Updated ${layoutFile} with geist/font imports`);
844
+ if (fs10.existsSync(cssPath)) {
845
+ const patched = patchCssVariables(cssPath, {
846
+ "--font-display": "var(--font-geist-sans)",
847
+ "--font-body": "var(--font-geist-sans)"
848
+ });
849
+ if (patched.length > 0) {
850
+ console.log(` ${CHECK} Updated font variables in ${path11.relative(cwd, cssPath)}`);
851
+ }
852
+ removeCssImport(cssPath, "./fonts.css");
853
+ }
854
+ return true;
492
855
  }
493
856
 
494
857
  // src/commands/init.ts
495
- function buildConfig(project) {
858
+ function buildConfig(project, fontChoice) {
496
859
  let cssPath;
497
860
  if (project.existingCssFile) {
498
861
  cssPath = project.existingCssFile;
@@ -511,6 +874,10 @@ function buildConfig(project) {
511
874
  ui: "@/components/ui",
512
875
  lib: "@/lib",
513
876
  hooks: "@/hooks"
877
+ },
878
+ fonts: {
879
+ strategy: fontChoice.strategy,
880
+ ...fontChoice.useNextFont ? { nextFont: true } : {}
514
881
  }
515
882
  };
516
883
  }
@@ -518,7 +885,7 @@ function stripTailwindCoreImport(content) {
518
885
  return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
519
886
  }
520
887
  function insertImportLine(filePath, importStatement) {
521
- const content = fs6.readFileSync(filePath, "utf-8");
888
+ const content = fs11.readFileSync(filePath, "utf-8");
522
889
  const lines = content.split("\n");
523
890
  let lastImportIndex = -1;
524
891
  for (let i = 0; i < lines.length; i++) {
@@ -531,10 +898,100 @@ function insertImportLine(filePath, importStatement) {
531
898
  } else {
532
899
  lines.unshift(importStatement);
533
900
  }
534
- fs6.writeFileSync(filePath, lines.join("\n"), "utf-8");
901
+ fs11.writeFileSync(filePath, lines.join("\n"), "utf-8");
902
+ }
903
+ async function promptFontChoice(project, detected, skipPrompts) {
904
+ const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
905
+ const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
906
+ if (skipPrompts) {
907
+ return {
908
+ strategy: defaultStrategy,
909
+ useNextFont: project.framework === "next" && defaultStrategy === "stera-default"
910
+ };
911
+ }
912
+ if (hasExistingFonts) {
913
+ const sources = [];
914
+ if (detected.fontFamilies.length > 0) {
915
+ sources.push(detected.fontFamilies.join(", "));
916
+ }
917
+ if (detected.hasNextFont) sources.push("next/font");
918
+ if (detected.hasGoogleFontsLink) sources.push("Google Fonts");
919
+ console.log(` Detected existing fonts: ${sources.join(" via ")}
920
+ `);
921
+ }
922
+ const choices = [];
923
+ if (hasExistingFonts) {
924
+ choices.push({
925
+ value: "keep-existing",
926
+ name: "Keep my current fonts",
927
+ description: "Update CSS variables to use your existing fonts"
928
+ });
929
+ }
930
+ choices.push({
931
+ value: "stera-default",
932
+ name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
933
+ description: "Self-hosted, optimized variable fonts"
934
+ });
935
+ choices.push({
936
+ value: "skip",
937
+ name: "Skip font setup",
938
+ description: "Configure fonts manually later"
939
+ });
940
+ const strategy = await select({
941
+ message: "How would you like to set up fonts?",
942
+ choices,
943
+ default: defaultStrategy
944
+ });
945
+ let useNextFont = false;
946
+ if (project.framework === "next" && strategy === "stera-default") {
947
+ useNextFont = await confirm({
948
+ message: "Use next/font for automatic optimization? (recommended)",
949
+ default: true
950
+ });
951
+ }
952
+ console.log("");
953
+ return { strategy, useNextFont };
954
+ }
955
+ async function executeFontStrategy(fontChoice, project, detected, config, cwd) {
956
+ const cssDir = path12.dirname(path12.resolve(cwd, config.css));
957
+ const cssPath = path12.resolve(cwd, config.css);
958
+ if (fontChoice.strategy === "skip") {
959
+ return {};
960
+ }
961
+ if (fontChoice.strategy === "keep-existing") {
962
+ if (fs11.existsSync(cssPath)) {
963
+ const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
964
+ const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
965
+ const patched = patchCssVariables(cssPath, {
966
+ "--font-display": fontValue,
967
+ "--font-body": fontValue
968
+ });
969
+ const steraUiPath = path12.join(cssDir, "stera-ui.css");
970
+ if (fs11.existsSync(steraUiPath)) {
971
+ patchCssVariables(steraUiPath, {
972
+ "--font-display": fontValue,
973
+ "--font-body": fontValue
974
+ });
975
+ }
976
+ if (patched.length > 0) {
977
+ console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
978
+ }
979
+ }
980
+ return {};
981
+ }
982
+ if (fontChoice.useNextFont && project.layoutFile) {
983
+ await installDependencies(["geist"], cwd);
984
+ const targetCss = fs11.existsSync(path12.join(cssDir, "stera-ui.css")) ? path12.join(cssDir, "stera-ui.css") : cssPath;
985
+ setupFontsNextjs(cwd, project.layoutFile, targetCss);
986
+ return {};
987
+ } else {
988
+ await installDependencies(["geist"], cwd);
989
+ const result = setupFontsGeneric(cwd, cssDir);
990
+ return { preloadTags: result.preloadTags };
991
+ }
535
992
  }
536
993
  async function init(options) {
537
- const cwd = options.cwd ? path8.resolve(options.cwd) : process.cwd();
994
+ const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
538
995
  console.log(`
539
996
  ${LOGO} Stera UI
540
997
  `);
@@ -544,8 +1001,8 @@ async function init(options) {
544
1001
  console.log(' Run "stera-ui add <component>" to add components.\n');
545
1002
  return;
546
1003
  }
547
- const pkgPath = path8.join(cwd, "package.json");
548
- if (!fs6.existsSync(pkgPath)) {
1004
+ const pkgPath = path12.join(cwd, "package.json");
1005
+ if (!fs11.existsSync(pkgPath)) {
549
1006
  console.error(
550
1007
  " Error: No package.json found. Please run this in a project directory."
551
1008
  );
@@ -560,47 +1017,41 @@ async function init(options) {
560
1017
  console.log(` Detected ${parts.join(" project with ")} project
561
1018
  `);
562
1019
  }
563
- const config = buildConfig(project);
564
- const configPath = path8.join(cwd, CONFIG_FILE);
565
- fs6.writeFileSync(
1020
+ const detectedFonts = detectFonts(cwd, project);
1021
+ const fontChoice = await promptFontChoice(
1022
+ project,
1023
+ detectedFonts,
1024
+ options.yes ?? false
1025
+ );
1026
+ const config = buildConfig(project, fontChoice);
1027
+ const configPath = path12.join(cwd, CONFIG_FILE);
1028
+ fs11.writeFileSync(
566
1029
  configPath,
567
1030
  JSON.stringify(config, null, 2) + "\n",
568
1031
  "utf-8"
569
1032
  );
570
1033
  console.log(` ${CHECK} Created components.json`);
571
- const cssDir = path8.dirname(path8.resolve(cwd, config.css));
1034
+ const cssDir = path12.dirname(path12.resolve(cwd, config.css));
572
1035
  if (project.existingCssFile) {
573
1036
  if (hasGlobalsCss(config, cwd)) {
574
1037
  console.log(
575
- ` ${CHECK} Stera already initialised ${dim(`(${path8.join(path8.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
1038
+ ` ${CHECK} Stera already initialised ${dim(`(${path12.join(path12.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
576
1039
  );
577
1040
  } else {
578
1041
  try {
579
1042
  const globalsItem = await fetchRegistryItem("globals");
580
1043
  const globalsContent = globalsItem.files[0].content ?? "";
581
1044
  const steraUiContent = stripTailwindCoreImport(globalsContent);
582
- const steraUiPath = path8.join(cssDir, "stera-ui.css");
583
- fs6.mkdirSync(cssDir, { recursive: true });
584
- fs6.writeFileSync(steraUiPath, steraUiContent, "utf-8");
585
- console.log(` ${CHECK} Created ${path8.relative(cwd, steraUiPath)}`);
586
- const existingCssPath = path8.resolve(cwd, project.existingCssFile);
1045
+ const steraUiPath = path12.join(cssDir, "stera-ui.css");
1046
+ fs11.mkdirSync(cssDir, { recursive: true });
1047
+ fs11.writeFileSync(steraUiPath, steraUiContent, "utf-8");
1048
+ console.log(` ${CHECK} Created ${path12.relative(cwd, steraUiPath)}`);
1049
+ const existingCssPath = path12.resolve(cwd, project.existingCssFile);
587
1050
  insertImportLine(existingCssPath, "@import './stera-ui.css';");
588
1051
  console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
589
1052
  } catch {
590
1053
  }
591
1054
  }
592
- try {
593
- const fontsItem = await fetchRegistryItem("fonts");
594
- const fontsPath = path8.join(cssDir, "fonts.css");
595
- fs6.mkdirSync(path8.dirname(fontsPath), { recursive: true });
596
- if (!fs6.existsSync(fontsPath)) {
597
- fs6.writeFileSync(fontsPath, fontsItem.files[0].content ?? "", "utf-8");
598
- console.log(` ${CHECK} Created ${path8.relative(cwd, fontsPath)}`);
599
- } else {
600
- console.log(` ${dim(path8.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
601
- }
602
- } catch {
603
- }
604
1055
  } else {
605
1056
  const resolved = await resolveDependencies(["globals"]);
606
1057
  const { written } = await writeComponentFiles(resolved, config, cwd);
@@ -608,6 +1059,13 @@ async function init(options) {
608
1059
  console.log(` ${CHECK} Created ${file}`);
609
1060
  }
610
1061
  }
1062
+ const fontResult = await executeFontStrategy(
1063
+ fontChoice,
1064
+ project,
1065
+ detectedFonts,
1066
+ config,
1067
+ cwd
1068
+ );
611
1069
  try {
612
1070
  const globalsItem = await fetchRegistryItem("globals");
613
1071
  if (globalsItem.dependencies) {
@@ -620,8 +1078,18 @@ async function init(options) {
620
1078
  console.log("");
621
1079
  console.log(" Next steps:");
622
1080
  console.log(" stera-ui add <component>");
623
- console.log("");
624
- console.log(` ${dim("See the README for font setup instructions.")}`);
1081
+ if (fontResult.preloadTags) {
1082
+ console.log("");
1083
+ console.log(` ${dim("For optimal font loading, add these to your HTML <head>:")}`);
1084
+ console.log("");
1085
+ for (const line of fontResult.preloadTags.split("\n")) {
1086
+ console.log(` ${dim(line)}`);
1087
+ }
1088
+ }
1089
+ if (fontChoice.strategy === "skip") {
1090
+ console.log("");
1091
+ console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
1092
+ }
625
1093
  console.log("");
626
1094
  }
627
1095
 
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  registryItemFileSchema,
12
12
  registryItemSchema,
13
13
  registryItemTypeSchema
14
- } from "./chunk-57DNZ2YS.js";
14
+ } from "./chunk-AXIACJ2C.js";
15
15
  import {
16
16
  CHECK,
17
17
  CONFIG_FILE,
@@ -31,7 +31,7 @@ import {
31
31
  resolveOutputPath,
32
32
  transformImports,
33
33
  writeComponentFiles
34
- } from "./chunk-PUJGGJCB.js";
34
+ } from "./chunk-MFUZBXO6.js";
35
35
  export {
36
36
  CHECK,
37
37
  CONFIG_FILE,
@@ -8,13 +8,13 @@ declare const registryItemFileSchema: z.ZodObject<{
8
8
  content: z.ZodOptional<z.ZodString>;
9
9
  target: z.ZodOptional<z.ZodString>;
10
10
  }, "strip", z.ZodTypeAny, {
11
- path: string;
12
11
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
12
+ path: string;
13
13
  content?: string | undefined;
14
14
  target?: string | undefined;
15
15
  }, {
16
- path: string;
17
16
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
17
+ path: string;
18
18
  content?: string | undefined;
19
19
  target?: string | undefined;
20
20
  }>;
@@ -30,33 +30,33 @@ declare const registryItemSchema: z.ZodObject<{
30
30
  content: z.ZodOptional<z.ZodString>;
31
31
  target: z.ZodOptional<z.ZodString>;
32
32
  }, "strip", z.ZodTypeAny, {
33
- path: string;
34
33
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
34
+ path: string;
35
35
  content?: string | undefined;
36
36
  target?: string | undefined;
37
37
  }, {
38
- path: string;
39
38
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
39
+ path: string;
40
40
  content?: string | undefined;
41
41
  target?: string | undefined;
42
42
  }>, "many">;
43
43
  }, "strip", z.ZodTypeAny, {
44
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
45
44
  name: string;
45
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
46
46
  files: {
47
- path: string;
48
47
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
48
+ path: string;
49
49
  content?: string | undefined;
50
50
  target?: string | undefined;
51
51
  }[];
52
52
  dependencies?: string[] | undefined;
53
53
  registryDependencies?: string[] | undefined;
54
54
  }, {
55
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
56
55
  name: string;
56
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
57
57
  files: {
58
- path: string;
59
58
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
59
+ path: string;
60
60
  content?: string | undefined;
61
61
  target?: string | undefined;
62
62
  }[];
@@ -77,33 +77,33 @@ declare const registryIndexSchema: z.ZodObject<{
77
77
  content: z.ZodOptional<z.ZodString>;
78
78
  target: z.ZodOptional<z.ZodString>;
79
79
  }, "strip", z.ZodTypeAny, {
80
- path: string;
81
80
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
81
+ path: string;
82
82
  content?: string | undefined;
83
83
  target?: string | undefined;
84
84
  }, {
85
- path: string;
86
85
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
86
+ path: string;
87
87
  content?: string | undefined;
88
88
  target?: string | undefined;
89
89
  }>, "many">;
90
90
  }, "strip", z.ZodTypeAny, {
91
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
92
91
  name: string;
92
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
93
93
  files: {
94
- path: string;
95
94
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
95
+ path: string;
96
96
  content?: string | undefined;
97
97
  target?: string | undefined;
98
98
  }[];
99
99
  dependencies?: string[] | undefined;
100
100
  registryDependencies?: string[] | undefined;
101
101
  }, {
102
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
103
102
  name: string;
103
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
104
104
  files: {
105
- path: string;
106
105
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
106
+ path: string;
107
107
  content?: string | undefined;
108
108
  target?: string | undefined;
109
109
  }[];
@@ -113,11 +113,11 @@ declare const registryIndexSchema: z.ZodObject<{
113
113
  }, "strip", z.ZodTypeAny, {
114
114
  name: string;
115
115
  items: {
116
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
117
116
  name: string;
117
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
118
118
  files: {
119
- path: string;
120
119
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
120
+ path: string;
121
121
  content?: string | undefined;
122
122
  target?: string | undefined;
123
123
  }[];
@@ -127,11 +127,11 @@ declare const registryIndexSchema: z.ZodObject<{
127
127
  }, {
128
128
  name: string;
129
129
  items: {
130
- type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
131
130
  name: string;
131
+ type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
132
132
  files: {
133
- path: string;
134
133
  type: "registry:ui" | "registry:component" | "registry:hook" | "registry:lib" | "registry:style";
134
+ path: string;
135
135
  content?: string | undefined;
136
136
  target?: string | undefined;
137
137
  }[];
@@ -163,6 +163,16 @@ declare const configSchema: z.ZodObject<{
163
163
  lib: string;
164
164
  hooks: string;
165
165
  }>;
166
+ fonts: z.ZodOptional<z.ZodObject<{
167
+ strategy: z.ZodEnum<["stera-default", "keep-existing", "skip"]>;
168
+ nextFont: z.ZodOptional<z.ZodBoolean>;
169
+ }, "strip", z.ZodTypeAny, {
170
+ strategy: "stera-default" | "keep-existing" | "skip";
171
+ nextFont?: boolean | undefined;
172
+ }, {
173
+ strategy: "stera-default" | "keep-existing" | "skip";
174
+ nextFont?: boolean | undefined;
175
+ }>>;
166
176
  }, "strip", z.ZodTypeAny, {
167
177
  version: 1;
168
178
  css: string;
@@ -174,6 +184,10 @@ declare const configSchema: z.ZodObject<{
174
184
  hooks: string;
175
185
  };
176
186
  $schema?: string | undefined;
187
+ fonts?: {
188
+ strategy: "stera-default" | "keep-existing" | "skip";
189
+ nextFont?: boolean | undefined;
190
+ } | undefined;
177
191
  }, {
178
192
  version: 1;
179
193
  css: string;
@@ -185,6 +199,10 @@ declare const configSchema: z.ZodObject<{
185
199
  hooks: string;
186
200
  };
187
201
  $schema?: string | undefined;
202
+ fonts?: {
203
+ strategy: "stera-default" | "keep-existing" | "skip";
204
+ nextFont?: boolean | undefined;
205
+ } | undefined;
188
206
  }>;
189
207
  type SteraConfig = z.infer<typeof configSchema>;
190
208
 
@@ -4,7 +4,7 @@ import {
4
4
  registryItemFileSchema,
5
5
  registryItemSchema,
6
6
  registryItemTypeSchema
7
- } from "../chunk-57DNZ2YS.js";
7
+ } from "../chunk-AXIACJ2C.js";
8
8
  export {
9
9
  configSchema,
10
10
  registryIndexSchema,
@@ -12,6 +12,10 @@ interface SteraConfig {
12
12
  lib: string;
13
13
  hooks: string;
14
14
  };
15
+ fonts?: {
16
+ strategy: "stera-default" | "keep-existing" | "skip";
17
+ nextFont?: boolean;
18
+ };
15
19
  }
16
20
  declare const CONFIG_FILE = "components.json";
17
21
  declare function findConfigPath(cwd: string): string | null;
@@ -50,6 +54,8 @@ interface ProjectInfo {
50
54
  framework: "next" | "vite" | "other";
51
55
  rsc: boolean;
52
56
  existingCssFile: string | null;
57
+ /** Path to the root layout file (layout.tsx for Next.js, index.html for Vite) */
58
+ layoutFile: string | null;
53
59
  }
54
60
  /**
55
61
  * Detect the project structure to inform configuration defaults.
@@ -69,7 +75,7 @@ declare function detectProject(cwd: string): ProjectInfo;
69
75
  declare function hasGlobalsCss(config: SteraConfig, projectRoot: string): boolean;
70
76
 
71
77
  type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
72
- declare function detectPackageManager(cwd: string): PackageManager;
78
+ declare function detectPackageManager(cwd: string): Promise<PackageManager>;
73
79
  /**
74
80
  * Install npm dependencies, skipping any already in the consumer's package.json.
75
81
  */
@@ -17,7 +17,7 @@ import {
17
17
  resolveOutputPath,
18
18
  transformImports,
19
19
  writeComponentFiles
20
- } from "../chunk-PUJGGJCB.js";
20
+ } from "../chunk-MFUZBXO6.js";
21
21
  export {
22
22
  CHECK,
23
23
  CONFIG_FILE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stera-ui",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Add Stera UI components to your project.",
6
6
  "license": "MIT",
@@ -33,7 +33,10 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
+ "@antfu/ni": "^30.0.0",
37
+ "@inquirer/prompts": "^7.0.0",
36
38
  "commander": "^13.1.0",
39
+ "execa": "^9.6.1",
37
40
  "tsconfig-paths": "^4.2.0",
38
41
  "zod": "^3.24.0"
39
42
  },