stera-ui 0.1.3 → 0.2.1

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
@@ -287,6 +302,13 @@ async function installDependencies(deps, cwd) {
287
302
  spinner.succeed(`Installed with ${pm}`);
288
303
  } catch (error) {
289
304
  spinner.fail(`Install failed`);
305
+ if (error instanceof Error && "stdout" in error && typeof error.stdout === "string" && error.stdout.includes("ERR_PNPM_ADDING_TO_ROOT")) {
306
+ throw new Error(
307
+ `Cannot install dependencies at a pnpm workspace root.
308
+ Run stera-ui init from within a specific workspace package instead,
309
+ or use "pnpm add -w <package>" manually to install at the root.`
310
+ );
311
+ }
290
312
  throw error;
291
313
  }
292
314
  }
package/dist/cli.js CHANGED
@@ -286,6 +286,13 @@ async function installDependencies(deps, cwd) {
286
286
  spinner.succeed(`Installed with ${pm}`);
287
287
  } catch (error) {
288
288
  spinner.fail(`Install failed`);
289
+ if (error instanceof Error && "stdout" in error && typeof error.stdout === "string" && error.stdout.includes("ERR_PNPM_ADDING_TO_ROOT")) {
290
+ throw new Error(
291
+ `Cannot install dependencies at a pnpm workspace root.
292
+ Run stera-ui init from within a specific workspace package instead,
293
+ or use "pnpm add -w <package>" manually to install at the root.`
294
+ );
295
+ }
289
296
  throw error;
290
297
  }
291
298
  }
@@ -444,8 +451,9 @@ async function add(components, options) {
444
451
  }
445
452
 
446
453
  // src/commands/init.ts
447
- import fs6 from "fs";
448
- import path8 from "path";
454
+ import fs11 from "fs";
455
+ import path12 from "path";
456
+ import { select, confirm } from "@inquirer/prompts";
449
457
 
450
458
  // src/utils/detect-project.ts
451
459
  import fs5 from "fs";
@@ -486,11 +494,375 @@ function detectProject(cwd) {
486
494
  rsc = fs5.existsSync(path7.join(cwd, appDir));
487
495
  }
488
496
  const existingCssFile = findTailwindCssFile(cwd);
489
- return { hasSrc, framework, rsc, existingCssFile };
497
+ let layoutFile = null;
498
+ if (framework === "next") {
499
+ const appDir = hasSrc ? "src/app" : "app";
500
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
501
+ const candidate = `${appDir}/layout.${ext}`;
502
+ if (fs5.existsSync(path7.join(cwd, candidate))) {
503
+ layoutFile = candidate;
504
+ break;
505
+ }
506
+ }
507
+ } else if (framework === "vite") {
508
+ if (fs5.existsSync(path7.join(cwd, "index.html"))) {
509
+ layoutFile = "index.html";
510
+ }
511
+ }
512
+ return { hasSrc, framework, rsc, existingCssFile, layoutFile };
513
+ }
514
+
515
+ // src/utils/detect-fonts.ts
516
+ import fs6 from "fs";
517
+ import path8 from "path";
518
+ function readIfExists(filePath) {
519
+ try {
520
+ return fs6.readFileSync(filePath, "utf-8");
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+ function detectNextFont(layoutContent) {
526
+ const hasNextFont = /import.*from\s+['"]next\/font/.test(layoutContent) || /import.*from\s+['"]@next\/font/.test(layoutContent) || /import.*from\s+['"]geist\/font/.test(layoutContent);
527
+ let nextFontVariable = null;
528
+ const varMatch = layoutContent.match(/variable:\s*['"](--.+?)['"]/);
529
+ if (varMatch) {
530
+ nextFontVariable = varMatch[1];
531
+ }
532
+ return { hasNextFont, nextFontVariable };
533
+ }
534
+ function detectFontFace(cssContent) {
535
+ const hasFontFace = /@font-face\s*\{/.test(cssContent);
536
+ const fontFamilies = [];
537
+ const regex = /font-family:\s*['"]?([^'";}\n]+?)['"]?\s*;/g;
538
+ let match;
539
+ while ((match = regex.exec(cssContent)) !== null) {
540
+ const family = match[1].trim();
541
+ if (family && !fontFamilies.includes(family)) {
542
+ fontFamilies.push(family);
543
+ }
544
+ }
545
+ return { hasFontFace, fontFamilies };
546
+ }
547
+ function detectGoogleFonts(content) {
548
+ return content.includes("fonts.googleapis.com") || content.includes("fonts.gstatic.com");
549
+ }
550
+ function detectFonts(cwd, project) {
551
+ const result = {
552
+ hasNextFont: false,
553
+ hasFontFace: false,
554
+ hasGoogleFontsLink: false,
555
+ fontFamilies: [],
556
+ nextFontVariable: null
557
+ };
558
+ if (project.layoutFile) {
559
+ const layoutContent = readIfExists(path8.join(cwd, project.layoutFile));
560
+ if (layoutContent) {
561
+ const nextFontResult = detectNextFont(layoutContent);
562
+ result.hasNextFont = nextFontResult.hasNextFont;
563
+ result.nextFontVariable = nextFontResult.nextFontVariable;
564
+ if (detectGoogleFonts(layoutContent)) {
565
+ result.hasGoogleFontsLink = true;
566
+ }
567
+ }
568
+ }
569
+ if (project.existingCssFile) {
570
+ const cssContent = readIfExists(path8.join(cwd, project.existingCssFile));
571
+ if (cssContent) {
572
+ const fontFaceResult = detectFontFace(cssContent);
573
+ result.hasFontFace = fontFaceResult.hasFontFace;
574
+ result.fontFamilies.push(...fontFaceResult.fontFamilies);
575
+ if (detectGoogleFonts(cssContent)) {
576
+ result.hasGoogleFontsLink = true;
577
+ }
578
+ const importRegex = /@import\s+['"](.+?)['"]/g;
579
+ let importMatch;
580
+ while ((importMatch = importRegex.exec(cssContent)) !== null) {
581
+ const importPath = importMatch[1];
582
+ const resolvedPath = path8.join(
583
+ cwd,
584
+ path8.dirname(project.existingCssFile),
585
+ importPath
586
+ );
587
+ const importedContent = readIfExists(resolvedPath);
588
+ if (importedContent) {
589
+ const imported = detectFontFace(importedContent);
590
+ if (imported.hasFontFace) result.hasFontFace = true;
591
+ for (const family of imported.fontFamilies) {
592
+ if (!result.fontFamilies.includes(family)) {
593
+ result.fontFamilies.push(family);
594
+ }
595
+ }
596
+ }
597
+ }
598
+ }
599
+ }
600
+ for (const htmlFile of ["index.html", "src/index.html"]) {
601
+ const htmlContent = readIfExists(path8.join(cwd, htmlFile));
602
+ if (htmlContent && detectGoogleFonts(htmlContent)) {
603
+ result.hasGoogleFontsLink = true;
604
+ }
605
+ }
606
+ return result;
607
+ }
608
+
609
+ // src/utils/patch-css-vars.ts
610
+ import fs7 from "fs";
611
+ function patchCssVariables(filePath, patches) {
612
+ const content = fs7.readFileSync(filePath, "utf-8");
613
+ const lines = content.split("\n");
614
+ const patched = [];
615
+ for (let i = 0; i < lines.length; i++) {
616
+ for (const [prop, newValue] of Object.entries(patches)) {
617
+ const regex = new RegExp(`(\\s*${escapeRegex(prop)}:\\s*).+?(;)`);
618
+ if (regex.test(lines[i])) {
619
+ lines[i] = lines[i].replace(regex, `$1${newValue}$2`);
620
+ if (!patched.includes(prop)) patched.push(prop);
621
+ }
622
+ }
623
+ }
624
+ if (patched.length > 0) {
625
+ fs7.writeFileSync(filePath, lines.join("\n"), "utf-8");
626
+ }
627
+ return patched;
628
+ }
629
+ function removeCssImport(filePath, importPath) {
630
+ const content = fs7.readFileSync(filePath, "utf-8");
631
+ const pattern = new RegExp(
632
+ `@import\\s+['"]${escapeRegex(importPath)}['"]\\s*;?\\n?`
633
+ );
634
+ if (!pattern.test(content)) return false;
635
+ fs7.writeFileSync(filePath, content.replace(pattern, ""), "utf-8");
636
+ return true;
637
+ }
638
+ function escapeRegex(str) {
639
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
640
+ }
641
+
642
+ // src/utils/setup-fonts-generic.ts
643
+ import fs9 from "fs";
644
+ import path10 from "path";
645
+
646
+ // src/utils/copy-fonts.ts
647
+ import fs8 from "fs";
648
+ import path9 from "path";
649
+ import { createRequire } from "module";
650
+ var FONT_FILES = [
651
+ {
652
+ src: "geist-sans/Geist-Variable.woff2",
653
+ dest: "geist/Geist-Variable.woff2"
654
+ },
655
+ {
656
+ src: "geist-sans/Geist-Italic[wght].woff2",
657
+ dest: "geist/Geist-Italic.woff2"
658
+ },
659
+ {
660
+ src: "geist-mono/GeistMono-Variable.woff2",
661
+ dest: "geist-mono/GeistMono-Variable.woff2"
662
+ },
663
+ {
664
+ src: "geist-mono/GeistMono-Italic[wght].woff2",
665
+ dest: "geist-mono/GeistMono-Italic.woff2"
666
+ }
667
+ ];
668
+ function resolveGeistFontsDir(cwd) {
669
+ const require2 = createRequire(path9.join(cwd, "package.json"));
670
+ const geistEntry = require2.resolve("geist/font/sans");
671
+ const distDir = path9.dirname(geistEntry);
672
+ return path9.join(distDir, "fonts");
673
+ }
674
+ function copyFonts(cwd) {
675
+ const spinner = createSpinner("Copying font files");
676
+ const fontsDir = resolveGeistFontsDir(cwd);
677
+ const publicFontsDir = path9.join(cwd, "public", "fonts");
678
+ const written = [];
679
+ for (const file of FONT_FILES) {
680
+ const srcPath = path9.join(fontsDir, file.src);
681
+ const destPath = path9.join(publicFontsDir, file.dest);
682
+ if (fs8.existsSync(destPath)) continue;
683
+ if (!fs8.existsSync(srcPath)) {
684
+ spinner.fail(`Font file not found: ${file.src}`);
685
+ throw new Error(
686
+ `Could not find ${srcPath}. Is the geist package installed?`
687
+ );
688
+ }
689
+ fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
690
+ fs8.copyFileSync(srcPath, destPath);
691
+ written.push(path9.relative(cwd, destPath));
692
+ }
693
+ if (written.length > 0) {
694
+ spinner.succeed(`Copied ${written.length} font files to public/fonts/`);
695
+ } else {
696
+ spinner.succeed("Font files already exist");
697
+ }
698
+ return written;
699
+ }
700
+
701
+ // src/utils/setup-fonts-generic.ts
702
+ 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
703
+ Stera UI font registry
704
+ To add a font:
705
+ 1. Drop .woff2 file(s) into public/fonts/{family-name}/
706
+ 2. Add an @font-face block below
707
+ 3. Add a --font-* variable in globals.css
708
+ \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 */
709
+
710
+ /* \u2500\u2500\u2500 Geist Sans \u2500\u2500\u2500 */
711
+ @font-face {
712
+ font-family: 'Geist';
713
+ src: url('/fonts/geist/Geist-Variable.woff2') format('woff2');
714
+ font-weight: 100 900;
715
+ font-style: normal;
716
+ font-display: swap;
717
+ }
718
+
719
+ @font-face {
720
+ font-family: 'Geist';
721
+ src: url('/fonts/geist/Geist-Italic.woff2') format('woff2');
722
+ font-weight: 100 900;
723
+ font-style: italic;
724
+ font-display: swap;
725
+ }
726
+
727
+ /* \u2500\u2500\u2500 Geist Mono \u2500\u2500\u2500 */
728
+ @font-face {
729
+ font-family: 'Geist Mono';
730
+ src: url('/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2');
731
+ font-weight: 100 900;
732
+ font-style: normal;
733
+ font-display: swap;
734
+ }
735
+
736
+ @font-face {
737
+ font-family: 'Geist Mono';
738
+ src: url('/fonts/geist-mono/GeistMono-Italic.woff2') format('woff2');
739
+ font-weight: 100 900;
740
+ font-style: italic;
741
+ font-display: swap;
742
+ }
743
+ `;
744
+ var PRELOAD_TAGS = `<link rel="preload" href="/fonts/geist/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
745
+ <link rel="preload" href="/fonts/geist-mono/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin />`;
746
+ function setupFontsGeneric(cwd, cssDir) {
747
+ copyFonts(cwd);
748
+ const fontsPath = path10.join(cssDir, "fonts.css");
749
+ if (!fs9.existsSync(fontsPath)) {
750
+ fs9.mkdirSync(path10.dirname(fontsPath), { recursive: true });
751
+ fs9.writeFileSync(fontsPath, FONTS_CSS_CONTENT, "utf-8");
752
+ console.log(` ${CHECK} Created ${path10.relative(cwd, fontsPath)}`);
753
+ } else {
754
+ console.log(` ${dim(path10.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
755
+ }
756
+ return { preloadTags: PRELOAD_TAGS };
757
+ }
758
+
759
+ // src/utils/setup-fonts-nextjs.ts
760
+ import fs10 from "fs";
761
+ import path11 from "path";
762
+ var GEIST_IMPORTS = `import { GeistSans } from "geist/font/sans"
763
+ import { GeistMono } from "geist/font/mono"`;
764
+ function canPatchLayout(content) {
765
+ if (!/<html[\s>]/.test(content)) return false;
766
+ if ((content.match(/<html[\s>]/g) || []).length > 1) return false;
767
+ return true;
768
+ }
769
+ function insertImports(content, imports) {
770
+ const lines = content.split("\n");
771
+ let lastImportIndex = -1;
772
+ for (let i = 0; i < lines.length; i++) {
773
+ const trimmed = lines[i].trim();
774
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import ")) {
775
+ lastImportIndex = i;
776
+ }
777
+ }
778
+ if (lastImportIndex >= 0) {
779
+ lines.splice(lastImportIndex + 1, 0, imports);
780
+ } else {
781
+ lines.unshift(imports);
782
+ }
783
+ return lines.join("\n");
784
+ }
785
+ function addClassNameToHtml(content) {
786
+ const fontClasses = "${GeistSans.variable} ${GeistMono.variable}";
787
+ const templateMatch = content.match(
788
+ /(<html[^>]*className=\{`)(.*?)(`\})/s
789
+ );
790
+ if (templateMatch) {
791
+ const existing = templateMatch[2].trim();
792
+ const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
793
+ return content.replace(
794
+ templateMatch[0],
795
+ `${templateMatch[1]}${newClassName}${templateMatch[3]}`
796
+ );
797
+ }
798
+ const exprMatch = content.match(
799
+ /(<html[^>]*className=\{)([^}`]+)(\})/
800
+ );
801
+ if (exprMatch) {
802
+ return content.replace(
803
+ exprMatch[0],
804
+ `${exprMatch[1]}\`\${${exprMatch[2].trim()}} ${fontClasses}\`${exprMatch[3]}`
805
+ );
806
+ }
807
+ const stringMatch = content.match(
808
+ /(<html[^>]*className=)(["'])([^"']*)(["'])/
809
+ );
810
+ if (stringMatch) {
811
+ const existing = stringMatch[3].trim();
812
+ const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
813
+ return content.replace(
814
+ stringMatch[0],
815
+ `${stringMatch[1]}{\`${newClassName}\`}`
816
+ );
817
+ }
818
+ return content.replace(
819
+ /<html(\s)/,
820
+ `<html className={\`${fontClasses}\`}$1`
821
+ );
822
+ }
823
+ function generateSnippet() {
824
+ return `
825
+ Add these imports to your root layout file:
826
+
827
+ ${dim(GEIST_IMPORTS)}
828
+
829
+ Then add the font variables to your <html> tag:
830
+
831
+ ${dim("<html className={`${GeistSans.variable} ${GeistMono.variable}`}>")}
832
+ `;
833
+ }
834
+ function setupFontsNextjs(cwd, layoutFile, cssFile) {
835
+ const layoutPath = path11.join(cwd, layoutFile);
836
+ const cssPath = path11.join(cwd, cssFile);
837
+ let layoutContent = fs10.readFileSync(layoutPath, "utf-8");
838
+ if (layoutContent.includes("geist/font/sans") || layoutContent.includes("GeistSans")) {
839
+ console.log(` ${dim("geist/font already configured in " + layoutFile)}`);
840
+ return true;
841
+ }
842
+ if (!canPatchLayout(layoutContent)) {
843
+ console.log(` ${WARN} Could not auto-patch ${layoutFile}`);
844
+ console.log(generateSnippet());
845
+ return false;
846
+ }
847
+ layoutContent = insertImports(layoutContent, GEIST_IMPORTS);
848
+ layoutContent = addClassNameToHtml(layoutContent);
849
+ fs10.writeFileSync(layoutPath, layoutContent, "utf-8");
850
+ console.log(` ${CHECK} Updated ${layoutFile} with geist/font imports`);
851
+ if (fs10.existsSync(cssPath)) {
852
+ const patched = patchCssVariables(cssPath, {
853
+ "--font-display": "var(--font-geist-sans)",
854
+ "--font-body": "var(--font-geist-sans)"
855
+ });
856
+ if (patched.length > 0) {
857
+ console.log(` ${CHECK} Updated font variables in ${path11.relative(cwd, cssPath)}`);
858
+ }
859
+ removeCssImport(cssPath, "./fonts.css");
860
+ }
861
+ return true;
490
862
  }
491
863
 
492
864
  // src/commands/init.ts
493
- function buildConfig(project) {
865
+ function buildConfig(project, fontChoice) {
494
866
  let cssPath;
495
867
  if (project.existingCssFile) {
496
868
  cssPath = project.existingCssFile;
@@ -509,6 +881,10 @@ function buildConfig(project) {
509
881
  ui: "@/components/ui",
510
882
  lib: "@/lib",
511
883
  hooks: "@/hooks"
884
+ },
885
+ fonts: {
886
+ strategy: fontChoice.strategy,
887
+ ...fontChoice.useNextFont ? { nextFont: true } : {}
512
888
  }
513
889
  };
514
890
  }
@@ -516,7 +892,7 @@ function stripTailwindCoreImport(content) {
516
892
  return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
517
893
  }
518
894
  function insertImportLine(filePath, importStatement) {
519
- const content = fs6.readFileSync(filePath, "utf-8");
895
+ const content = fs11.readFileSync(filePath, "utf-8");
520
896
  const lines = content.split("\n");
521
897
  let lastImportIndex = -1;
522
898
  for (let i = 0; i < lines.length; i++) {
@@ -529,10 +905,100 @@ function insertImportLine(filePath, importStatement) {
529
905
  } else {
530
906
  lines.unshift(importStatement);
531
907
  }
532
- fs6.writeFileSync(filePath, lines.join("\n"), "utf-8");
908
+ fs11.writeFileSync(filePath, lines.join("\n"), "utf-8");
909
+ }
910
+ async function promptFontChoice(project, detected, skipPrompts) {
911
+ const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
912
+ const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
913
+ if (skipPrompts) {
914
+ return {
915
+ strategy: defaultStrategy,
916
+ useNextFont: project.framework === "next" && defaultStrategy === "stera-default"
917
+ };
918
+ }
919
+ if (hasExistingFonts) {
920
+ const sources = [];
921
+ if (detected.fontFamilies.length > 0) {
922
+ sources.push(detected.fontFamilies.join(", "));
923
+ }
924
+ if (detected.hasNextFont) sources.push("next/font");
925
+ if (detected.hasGoogleFontsLink) sources.push("Google Fonts");
926
+ console.log(` Detected existing fonts: ${sources.join(" via ")}
927
+ `);
928
+ }
929
+ const choices = [];
930
+ if (hasExistingFonts) {
931
+ choices.push({
932
+ value: "keep-existing",
933
+ name: "Keep my current fonts",
934
+ description: "Update CSS variables to use your existing fonts"
935
+ });
936
+ }
937
+ choices.push({
938
+ value: "stera-default",
939
+ name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
940
+ description: "Self-hosted, optimized variable fonts"
941
+ });
942
+ choices.push({
943
+ value: "skip",
944
+ name: "Skip font setup",
945
+ description: "Configure fonts manually later"
946
+ });
947
+ const strategy = await select({
948
+ message: "How would you like to set up fonts?",
949
+ choices,
950
+ default: defaultStrategy
951
+ });
952
+ let useNextFont = false;
953
+ if (project.framework === "next" && strategy === "stera-default") {
954
+ useNextFont = await confirm({
955
+ message: "Use next/font for automatic optimization? (recommended)",
956
+ default: true
957
+ });
958
+ }
959
+ console.log("");
960
+ return { strategy, useNextFont };
961
+ }
962
+ async function executeFontStrategy(fontChoice, project, detected, config, cwd) {
963
+ const cssDir = path12.dirname(path12.resolve(cwd, config.css));
964
+ const cssPath = path12.resolve(cwd, config.css);
965
+ if (fontChoice.strategy === "skip") {
966
+ return {};
967
+ }
968
+ if (fontChoice.strategy === "keep-existing") {
969
+ if (fs11.existsSync(cssPath)) {
970
+ const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
971
+ const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
972
+ const patched = patchCssVariables(cssPath, {
973
+ "--font-display": fontValue,
974
+ "--font-body": fontValue
975
+ });
976
+ const steraUiPath = path12.join(cssDir, "stera-ui.css");
977
+ if (fs11.existsSync(steraUiPath)) {
978
+ patchCssVariables(steraUiPath, {
979
+ "--font-display": fontValue,
980
+ "--font-body": fontValue
981
+ });
982
+ }
983
+ if (patched.length > 0) {
984
+ console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
985
+ }
986
+ }
987
+ return {};
988
+ }
989
+ if (fontChoice.useNextFont && project.layoutFile) {
990
+ await installDependencies(["geist"], cwd);
991
+ const targetCss = fs11.existsSync(path12.join(cssDir, "stera-ui.css")) ? path12.join(cssDir, "stera-ui.css") : cssPath;
992
+ setupFontsNextjs(cwd, project.layoutFile, targetCss);
993
+ return {};
994
+ } else {
995
+ await installDependencies(["geist"], cwd);
996
+ const result = setupFontsGeneric(cwd, cssDir);
997
+ return { preloadTags: result.preloadTags };
998
+ }
533
999
  }
534
1000
  async function init(options) {
535
- const cwd = options.cwd ? path8.resolve(options.cwd) : process.cwd();
1001
+ const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
536
1002
  console.log(`
537
1003
  ${LOGO} Stera UI
538
1004
  `);
@@ -542,8 +1008,8 @@ async function init(options) {
542
1008
  console.log(' Run "stera-ui add <component>" to add components.\n');
543
1009
  return;
544
1010
  }
545
- const pkgPath = path8.join(cwd, "package.json");
546
- if (!fs6.existsSync(pkgPath)) {
1011
+ const pkgPath = path12.join(cwd, "package.json");
1012
+ if (!fs11.existsSync(pkgPath)) {
547
1013
  console.error(
548
1014
  " Error: No package.json found. Please run this in a project directory."
549
1015
  );
@@ -558,47 +1024,41 @@ async function init(options) {
558
1024
  console.log(` Detected ${parts.join(" project with ")} project
559
1025
  `);
560
1026
  }
561
- const config = buildConfig(project);
562
- const configPath = path8.join(cwd, CONFIG_FILE);
563
- fs6.writeFileSync(
1027
+ const detectedFonts = detectFonts(cwd, project);
1028
+ const fontChoice = await promptFontChoice(
1029
+ project,
1030
+ detectedFonts,
1031
+ options.yes ?? false
1032
+ );
1033
+ const config = buildConfig(project, fontChoice);
1034
+ const configPath = path12.join(cwd, CONFIG_FILE);
1035
+ fs11.writeFileSync(
564
1036
  configPath,
565
1037
  JSON.stringify(config, null, 2) + "\n",
566
1038
  "utf-8"
567
1039
  );
568
1040
  console.log(` ${CHECK} Created components.json`);
569
- const cssDir = path8.dirname(path8.resolve(cwd, config.css));
1041
+ const cssDir = path12.dirname(path12.resolve(cwd, config.css));
570
1042
  if (project.existingCssFile) {
571
1043
  if (hasGlobalsCss(config, cwd)) {
572
1044
  console.log(
573
- ` ${CHECK} Stera already initialised ${dim(`(${path8.join(path8.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
1045
+ ` ${CHECK} Stera already initialised ${dim(`(${path12.join(path12.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
574
1046
  );
575
1047
  } else {
576
1048
  try {
577
1049
  const globalsItem = await fetchRegistryItem("globals");
578
1050
  const globalsContent = globalsItem.files[0].content ?? "";
579
1051
  const steraUiContent = stripTailwindCoreImport(globalsContent);
580
- const steraUiPath = path8.join(cssDir, "stera-ui.css");
581
- fs6.mkdirSync(cssDir, { recursive: true });
582
- fs6.writeFileSync(steraUiPath, steraUiContent, "utf-8");
583
- console.log(` ${CHECK} Created ${path8.relative(cwd, steraUiPath)}`);
584
- const existingCssPath = path8.resolve(cwd, project.existingCssFile);
1052
+ const steraUiPath = path12.join(cssDir, "stera-ui.css");
1053
+ fs11.mkdirSync(cssDir, { recursive: true });
1054
+ fs11.writeFileSync(steraUiPath, steraUiContent, "utf-8");
1055
+ console.log(` ${CHECK} Created ${path12.relative(cwd, steraUiPath)}`);
1056
+ const existingCssPath = path12.resolve(cwd, project.existingCssFile);
585
1057
  insertImportLine(existingCssPath, "@import './stera-ui.css';");
586
1058
  console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
587
1059
  } catch {
588
1060
  }
589
1061
  }
590
- try {
591
- const fontsItem = await fetchRegistryItem("fonts");
592
- const fontsPath = path8.join(cssDir, "fonts.css");
593
- fs6.mkdirSync(path8.dirname(fontsPath), { recursive: true });
594
- if (!fs6.existsSync(fontsPath)) {
595
- fs6.writeFileSync(fontsPath, fontsItem.files[0].content ?? "", "utf-8");
596
- console.log(` ${CHECK} Created ${path8.relative(cwd, fontsPath)}`);
597
- } else {
598
- console.log(` ${dim(path8.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
599
- }
600
- } catch {
601
- }
602
1062
  } else {
603
1063
  const resolved = await resolveDependencies(["globals"]);
604
1064
  const { written } = await writeComponentFiles(resolved, config, cwd);
@@ -606,6 +1066,13 @@ async function init(options) {
606
1066
  console.log(` ${CHECK} Created ${file}`);
607
1067
  }
608
1068
  }
1069
+ const fontResult = await executeFontStrategy(
1070
+ fontChoice,
1071
+ project,
1072
+ detectedFonts,
1073
+ config,
1074
+ cwd
1075
+ );
609
1076
  try {
610
1077
  const globalsItem = await fetchRegistryItem("globals");
611
1078
  if (globalsItem.dependencies) {
@@ -618,8 +1085,18 @@ async function init(options) {
618
1085
  console.log("");
619
1086
  console.log(" Next steps:");
620
1087
  console.log(" stera-ui add <component>");
621
- console.log("");
622
- console.log(` ${dim("See the README for font setup instructions.")}`);
1088
+ if (fontResult.preloadTags) {
1089
+ console.log("");
1090
+ console.log(` ${dim("For optimal font loading, add these to your HTML <head>:")}`);
1091
+ console.log("");
1092
+ for (const line of fontResult.preloadTags.split("\n")) {
1093
+ console.log(` ${dim(line)}`);
1094
+ }
1095
+ }
1096
+ if (fontChoice.strategy === "skip") {
1097
+ console.log("");
1098
+ console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
1099
+ }
623
1100
  console.log("");
624
1101
  }
625
1102
 
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-HCHFPFTS.js";
34
+ } from "./chunk-KIEC5SPW.js";
35
35
  export {
36
36
  CHECK,
37
37
  CONFIG_FILE,
@@ -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.
@@ -17,7 +17,7 @@ import {
17
17
  resolveOutputPath,
18
18
  transformImports,
19
19
  writeComponentFiles
20
- } from "../chunk-HCHFPFTS.js";
20
+ } from "../chunk-KIEC5SPW.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.3",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Add Stera UI components to your project.",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@antfu/ni": "^30.0.0",
37
+ "@inquirer/prompts": "^7.0.0",
37
38
  "commander": "^13.1.0",
38
39
  "execa": "^9.6.1",
39
40
  "tsconfig-paths": "^4.2.0",