stera-ui 0.1.3 → 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
package/dist/cli.js CHANGED
@@ -444,8 +444,9 @@ async function add(components, options) {
444
444
  }
445
445
 
446
446
  // src/commands/init.ts
447
- import fs6 from "fs";
448
- import path8 from "path";
447
+ import fs11 from "fs";
448
+ import path12 from "path";
449
+ import { select, confirm } from "@inquirer/prompts";
449
450
 
450
451
  // src/utils/detect-project.ts
451
452
  import fs5 from "fs";
@@ -486,11 +487,375 @@ function detectProject(cwd) {
486
487
  rsc = fs5.existsSync(path7.join(cwd, appDir));
487
488
  }
488
489
  const existingCssFile = findTailwindCssFile(cwd);
489
- 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;
490
855
  }
491
856
 
492
857
  // src/commands/init.ts
493
- function buildConfig(project) {
858
+ function buildConfig(project, fontChoice) {
494
859
  let cssPath;
495
860
  if (project.existingCssFile) {
496
861
  cssPath = project.existingCssFile;
@@ -509,6 +874,10 @@ function buildConfig(project) {
509
874
  ui: "@/components/ui",
510
875
  lib: "@/lib",
511
876
  hooks: "@/hooks"
877
+ },
878
+ fonts: {
879
+ strategy: fontChoice.strategy,
880
+ ...fontChoice.useNextFont ? { nextFont: true } : {}
512
881
  }
513
882
  };
514
883
  }
@@ -516,7 +885,7 @@ function stripTailwindCoreImport(content) {
516
885
  return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
517
886
  }
518
887
  function insertImportLine(filePath, importStatement) {
519
- const content = fs6.readFileSync(filePath, "utf-8");
888
+ const content = fs11.readFileSync(filePath, "utf-8");
520
889
  const lines = content.split("\n");
521
890
  let lastImportIndex = -1;
522
891
  for (let i = 0; i < lines.length; i++) {
@@ -529,10 +898,100 @@ function insertImportLine(filePath, importStatement) {
529
898
  } else {
530
899
  lines.unshift(importStatement);
531
900
  }
532
- 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
+ }
533
992
  }
534
993
  async function init(options) {
535
- const cwd = options.cwd ? path8.resolve(options.cwd) : process.cwd();
994
+ const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
536
995
  console.log(`
537
996
  ${LOGO} Stera UI
538
997
  `);
@@ -542,8 +1001,8 @@ async function init(options) {
542
1001
  console.log(' Run "stera-ui add <component>" to add components.\n');
543
1002
  return;
544
1003
  }
545
- const pkgPath = path8.join(cwd, "package.json");
546
- if (!fs6.existsSync(pkgPath)) {
1004
+ const pkgPath = path12.join(cwd, "package.json");
1005
+ if (!fs11.existsSync(pkgPath)) {
547
1006
  console.error(
548
1007
  " Error: No package.json found. Please run this in a project directory."
549
1008
  );
@@ -558,47 +1017,41 @@ async function init(options) {
558
1017
  console.log(` Detected ${parts.join(" project with ")} project
559
1018
  `);
560
1019
  }
561
- const config = buildConfig(project);
562
- const configPath = path8.join(cwd, CONFIG_FILE);
563
- 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(
564
1029
  configPath,
565
1030
  JSON.stringify(config, null, 2) + "\n",
566
1031
  "utf-8"
567
1032
  );
568
1033
  console.log(` ${CHECK} Created components.json`);
569
- const cssDir = path8.dirname(path8.resolve(cwd, config.css));
1034
+ const cssDir = path12.dirname(path12.resolve(cwd, config.css));
570
1035
  if (project.existingCssFile) {
571
1036
  if (hasGlobalsCss(config, cwd)) {
572
1037
  console.log(
573
- ` ${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)`)}`
574
1039
  );
575
1040
  } else {
576
1041
  try {
577
1042
  const globalsItem = await fetchRegistryItem("globals");
578
1043
  const globalsContent = globalsItem.files[0].content ?? "";
579
1044
  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);
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);
585
1050
  insertImportLine(existingCssPath, "@import './stera-ui.css';");
586
1051
  console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
587
1052
  } catch {
588
1053
  }
589
1054
  }
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
1055
  } else {
603
1056
  const resolved = await resolveDependencies(["globals"]);
604
1057
  const { written } = await writeComponentFiles(resolved, config, cwd);
@@ -606,6 +1059,13 @@ async function init(options) {
606
1059
  console.log(` ${CHECK} Created ${file}`);
607
1060
  }
608
1061
  }
1062
+ const fontResult = await executeFontStrategy(
1063
+ fontChoice,
1064
+ project,
1065
+ detectedFonts,
1066
+ config,
1067
+ cwd
1068
+ );
609
1069
  try {
610
1070
  const globalsItem = await fetchRegistryItem("globals");
611
1071
  if (globalsItem.dependencies) {
@@ -618,8 +1078,18 @@ async function init(options) {
618
1078
  console.log("");
619
1079
  console.log(" Next steps:");
620
1080
  console.log(" stera-ui add <component>");
621
- console.log("");
622
- 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
+ }
623
1093
  console.log("");
624
1094
  }
625
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-HCHFPFTS.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.
@@ -17,7 +17,7 @@ import {
17
17
  resolveOutputPath,
18
18
  transformImports,
19
19
  writeComponentFiles
20
- } from "../chunk-HCHFPFTS.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.3",
3
+ "version": "0.2.0",
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",