stera-ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -153,7 +153,7 @@ async function writeComponentFiles(items, config, projectRoot, options = {}) {
153
153
  const written = [];
154
154
  const skipped = [];
155
155
  for (const item of items) {
156
- for (const file of item.files) {
156
+ for (const file of item.files ?? []) {
157
157
  const outputPath = resolveOutputPath(config, projectRoot, file);
158
158
  const relativePath = path3.relative(projectRoot, outputPath);
159
159
  if (fs2.existsSync(outputPath)) {
@@ -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
  }
@@ -394,7 +401,7 @@ async function add(components, options) {
394
401
  }
395
402
  const resolved = await resolveDependencies(components);
396
403
  for (const item of resolved) {
397
- for (const file of item.files) {
404
+ for (const file of item.files ?? []) {
398
405
  if (file.content) {
399
406
  file.content = applyTransforms(
400
407
  file.content,
@@ -444,9 +451,9 @@ async function add(components, options) {
444
451
  }
445
452
 
446
453
  // src/commands/init.ts
447
- import fs11 from "fs";
448
- import path12 from "path";
449
- import { select, confirm } from "@inquirer/prompts";
454
+ import fs9 from "fs";
455
+ import path10 from "path";
456
+ import { select } from "@inquirer/prompts";
450
457
 
451
458
  // src/utils/detect-project.ts
452
459
  import fs5 from "fs";
@@ -619,243 +626,324 @@ function patchCssVariables(filePath, patches) {
619
626
  }
620
627
  return patched;
621
628
  }
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
629
  function escapeRegex(str) {
632
630
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
633
631
  }
634
632
 
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";
633
+ // src/utils/update-fonts.ts
634
+ import { existsSync, promises as fs8 } from "fs";
641
635
  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");
636
+ import {
637
+ Project,
638
+ ScriptKind,
639
+ SyntaxKind,
640
+ VariableDeclarationKind
641
+ } from "ts-morph";
642
+ var ROOT_FONT_VARIABLES = /* @__PURE__ */ new Set([
643
+ "--font-sans",
644
+ "--font-serif",
645
+ "--font-mono"
646
+ ]);
647
+ function massageTreeForFonts(fonts, project) {
648
+ const result = {
649
+ cssVars: {},
650
+ dependencies: [],
651
+ cssImports: []
652
+ };
653
+ if (fonts.length === 0) return result;
654
+ const isNext = project.framework === "next";
655
+ for (const font of fonts) {
656
+ if (isNext) {
657
+ result.cssVars[font.font.variable] = `var(${font.font.variable})`;
658
+ } else {
659
+ const fontName = font.name.replace(/^font-/, "");
660
+ const fontSourceDep = font.font.dependency ?? `@fontsource-variable/${fontName}`;
661
+ result.dependencies.push(fontSourceDep);
662
+ result.cssImports.push(`@import "${fontSourceDep}";`);
663
+ result.cssVars[font.font.variable] = font.font.family;
664
+ }
665
+ }
666
+ return result;
666
667
  }
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?`
668
+ async function updateFonts(fonts, project, cwd, options = {}) {
669
+ if (fonts.length === 0) return;
670
+ if (project.framework !== "next") return;
671
+ const layoutPath = findLayoutFile(project, cwd);
672
+ if (!layoutPath) return;
673
+ if (options.silent) {
674
+ try {
675
+ const layoutContent = await fs8.readFile(layoutPath, "utf-8");
676
+ const updatedContent = transformLayoutFonts(layoutContent, fonts);
677
+ if (updatedContent !== layoutContent) {
678
+ await fs8.writeFile(layoutPath, updatedContent, "utf-8");
679
+ }
680
+ } catch {
681
+ }
682
+ return;
683
+ }
684
+ const spinner = createSpinner("Updating fonts");
685
+ try {
686
+ const layoutContent = await fs8.readFile(layoutPath, "utf-8");
687
+ const updatedContent = transformLayoutFonts(layoutContent, fonts);
688
+ if (updatedContent !== layoutContent) {
689
+ await fs8.writeFile(layoutPath, updatedContent, "utf-8");
690
+ }
691
+ spinner.succeed("Updated layout with font imports");
692
+ } catch {
693
+ spinner.fail("Failed to update fonts");
694
+ }
695
+ }
696
+ function findLayoutFile(project, cwd) {
697
+ if (!project.layoutFile) return null;
698
+ const fullPath = path9.join(cwd, project.layoutFile);
699
+ return existsSync(fullPath) ? fullPath : null;
700
+ }
701
+ function transformLayoutFonts(input, fonts) {
702
+ const project = new Project({ compilerOptions: {} });
703
+ const sourceFile = project.createSourceFile("layout.tsx", input, {
704
+ scriptKind: ScriptKind.TSX
705
+ });
706
+ const googleFonts = fonts.filter((f) => f.font.provider === "google");
707
+ const fontVariableNames = [];
708
+ const fontUtilityClasses = [];
709
+ for (const font of googleFonts) {
710
+ const importName = font.font.import;
711
+ if (!importName) continue;
712
+ const existingImport = sourceFile.getImportDeclaration(
713
+ (decl) => decl.getModuleSpecifierValue() === "next/font/google"
714
+ );
715
+ let hasExistingImport = false;
716
+ if (existingImport) {
717
+ const namedImports = existingImport.getNamedImports();
718
+ hasExistingImport = namedImports.some(
719
+ (imp) => imp.getName() === importName
680
720
  );
721
+ if (!hasExistingImport) {
722
+ existingImport.addNamedImport(importName);
723
+ }
724
+ } else {
725
+ sourceFile.addImportDeclaration({
726
+ moduleSpecifier: "next/font/google",
727
+ namedImports: [importName]
728
+ });
729
+ }
730
+ const varName = getFontVariableName(importName, font.font.variable);
731
+ const fontOptions = buildFontOptions(font);
732
+ const existingVarDecl = findFontVariableDeclaration(
733
+ sourceFile,
734
+ font.font.variable
735
+ );
736
+ if (hasExistingImport && !existingVarDecl && isRootFontVariable(font.font.variable)) {
737
+ continue;
738
+ }
739
+ if (existingVarDecl) {
740
+ existingVarDecl.setInitializer(`${importName}(${fontOptions})`);
741
+ if (existingVarDecl.getName() !== varName) {
742
+ existingVarDecl.rename(varName);
743
+ }
744
+ } else {
745
+ const insertPosition = findInsertPosition(sourceFile);
746
+ const statement = sourceFile.insertVariableStatement(insertPosition, {
747
+ declarationKind: VariableDeclarationKind.Const,
748
+ declarations: [
749
+ {
750
+ name: varName,
751
+ initializer: `${importName}(${fontOptions})`
752
+ }
753
+ ]
754
+ });
755
+ statement.appendWhitespace("\n");
756
+ }
757
+ fontVariableNames.push(varName);
758
+ if (shouldApplyFontUtilityToHtml(font)) {
759
+ fontUtilityClasses.push(font.font.variable.replace("--", ""));
681
760
  }
682
- fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
683
- fs8.copyFileSync(srcPath, destPath);
684
- written.push(path9.relative(cwd, destPath));
685
761
  }
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");
762
+ const fontFamilyClasses = /* @__PURE__ */ new Set(["font-sans", "font-serif", "font-mono"]);
763
+ const lastFontFamilyClass = [...fontUtilityClasses].reverse().find((cls) => fontFamilyClasses.has(cls));
764
+ const filteredUtilityClasses = fontUtilityClasses.filter(
765
+ (cls) => !fontFamilyClasses.has(cls)
766
+ );
767
+ if (lastFontFamilyClass) {
768
+ filteredUtilityClasses.unshift(lastFontFamilyClass);
769
+ }
770
+ if (fontVariableNames.length > 0) {
771
+ updateHtmlClassName(sourceFile, fontVariableNames, filteredUtilityClasses);
690
772
  }
691
- return written;
773
+ return sourceFile.getFullText();
692
774
  }
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;
775
+ function buildFontOptions(font) {
776
+ const options = {};
777
+ if (font.font.subsets?.length) options.subsets = font.font.subsets;
778
+ if (font.font.weight?.length) options.weight = font.font.weight;
779
+ options.variable = font.font.variable;
780
+ return JSON.stringify(options).replace(/"([^"]+)":/g, "$1:").replace(/"/g, "'");
710
781
  }
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;
782
+ function isRootFontVariable(variable) {
783
+ return ROOT_FONT_VARIABLES.has(variable);
718
784
  }
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;
785
+ function shouldApplyFontUtilityToHtml(font) {
786
+ return !font.font.selector && isRootFontVariable(font.font.variable);
727
787
  }
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;
788
+ function getFontVariableName(importName, variable) {
789
+ const baseName = toCamelCase(importName);
790
+ if (isRootFontVariable(variable)) return baseName;
791
+ return `${baseName}${toPascalCase(variable.replace(/^--font-/, ""))}`;
735
792
  }
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 };
793
+ function toCamelCase(str) {
794
+ return str.split("_").map(
795
+ (part, index) => index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
796
+ ).join("");
750
797
  }
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;
798
+ function toPascalCase(str) {
799
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
761
800
  }
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;
801
+ function findFontVariableDeclaration(sourceFile, variable) {
802
+ for (const statement of sourceFile.getVariableStatements()) {
803
+ for (const declaration of statement.getDeclarations()) {
804
+ const initializer = declaration.getInitializer();
805
+ if (!initializer || initializer.getKind() !== SyntaxKind.CallExpression)
806
+ continue;
807
+ const args = initializer.getArguments();
808
+ if (args.length === 0) continue;
809
+ const argText = args[0].getText();
810
+ if (argText.includes("variable:") && argText.includes(variable)) {
811
+ return declaration;
812
+ }
769
813
  }
770
814
  }
771
- if (lastImportIndex >= 0) {
772
- lines.splice(lastImportIndex + 1, 0, imports);
773
- } else {
774
- lines.unshift(imports);
775
- }
776
- return lines.join("\n");
815
+ return null;
777
816
  }
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
- );
817
+ function findInsertPosition(sourceFile) {
818
+ const imports = sourceFile.getImportDeclarations();
819
+ if (imports.length > 0) {
820
+ return imports[imports.length - 1].getChildIndex() + 1;
790
821
  }
791
- const exprMatch = content.match(
792
- /(<html[^>]*className=\{)([^}`]+)(\})/
822
+ return 0;
823
+ }
824
+ function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
825
+ const jsxElements = sourceFile.getDescendantsOfKind(
826
+ SyntaxKind.JsxOpeningElement
793
827
  );
794
- if (exprMatch) {
795
- return content.replace(
796
- exprMatch[0],
797
- `${exprMatch[1]}\`\${${exprMatch[2].trim()}} ${fontClasses}\`${exprMatch[3]}`
828
+ for (const element of jsxElements) {
829
+ if (element.getTagNameNode().getText() !== "html") continue;
830
+ const newUtilityClasses = fontUtilityClasses.map((cls) => `"${cls}"`);
831
+ const newVarExpressions = fontVariableNames.map(
832
+ (name) => `${name}.variable`
798
833
  );
834
+ const allNewArgs = [...newUtilityClasses, ...newVarExpressions];
835
+ const classNameAttr = element.getAttribute("className");
836
+ if (!classNameAttr) {
837
+ element.addAttribute({
838
+ name: "className",
839
+ initializer: `{cn(${allNewArgs.join(", ")})}`
840
+ });
841
+ ensureCnImport(sourceFile);
842
+ return;
843
+ }
844
+ if (classNameAttr.getKind() !== SyntaxKind.JsxAttribute) return;
845
+ const jsxAttr = classNameAttr.asKindOrThrow(SyntaxKind.JsxAttribute);
846
+ const initializer = jsxAttr.getInitializer();
847
+ if (!initializer) return;
848
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
849
+ const currentValue = initializer.getText().slice(1, -1);
850
+ ensureCnImport(sourceFile);
851
+ jsxAttr.setInitializer(
852
+ `{cn("${currentValue}", ${allNewArgs.join(", ")})}`
853
+ );
854
+ } else if (initializer.getKind() === SyntaxKind.JsxExpression) {
855
+ const jsxExpr = initializer.asKindOrThrow(SyntaxKind.JsxExpression);
856
+ const expr = jsxExpr.getExpression();
857
+ if (!expr) return;
858
+ const exprText = expr.getText();
859
+ if (exprText.startsWith("cn(")) {
860
+ const hasAllFontVars = newVarExpressions.every(
861
+ (v) => exprText.includes(v)
862
+ );
863
+ const hasAllUtilityClasses = fontUtilityClasses.every(
864
+ (cls) => exprText.includes(`"${cls}"`)
865
+ );
866
+ if (hasAllFontVars && hasAllUtilityClasses) continue;
867
+ let cleanedExpr = exprText;
868
+ for (const varExpr of newVarExpressions) {
869
+ cleanedExpr = cleanedExpr.replace(
870
+ new RegExp(`,?\\s*${varExpr.replace(".", "\\.")}`, "g"),
871
+ ""
872
+ );
873
+ }
874
+ cleanedExpr = cleanedExpr.replace(/cn\(\s*,/, "cn(");
875
+ const newExpr = cleanedExpr.replace(
876
+ /\)$/,
877
+ `, ${allNewArgs.join(", ")})`
878
+ );
879
+ jsxExpr.replaceWithText(`{${newExpr}}`);
880
+ } else if (/^\w+\.variable$/.test(exprText)) {
881
+ ensureCnImport(sourceFile);
882
+ jsxExpr.replaceWithText(`{cn(${allNewArgs.join(", ")})}`);
883
+ } else if (exprText.startsWith("`") && exprText.endsWith("`")) {
884
+ const cnArgs = parseTemplateLiteralToCnArgs(exprText);
885
+ ensureCnImport(sourceFile);
886
+ const allNewArgsSet = new Set(allNewArgs);
887
+ const fontFamilyLiterals = new Set(
888
+ ["font-sans", "font-serif", "font-mono"].map((c) => `"${c}"`)
889
+ );
890
+ const cleanedCnArgs = cnArgs.filter(
891
+ (arg) => !allNewArgsSet.has(arg) && !fontFamilyLiterals.has(arg)
892
+ );
893
+ jsxExpr.replaceWithText(
894
+ `{cn(${[...cleanedCnArgs, ...allNewArgs].join(", ")})}`
895
+ );
896
+ } else {
897
+ ensureCnImport(sourceFile);
898
+ jsxExpr.replaceWithText(`{cn(${exprText}, ${allNewArgs.join(", ")})}`);
899
+ }
900
+ }
799
901
  }
800
- const stringMatch = content.match(
801
- /(<html[^>]*className=)(["'])([^"']*)(["'])/
902
+ }
903
+ function ensureCnImport(sourceFile) {
904
+ const existingImport = sourceFile.getImportDeclaration(
905
+ (decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
802
906
  );
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}\`}`
907
+ if (!existingImport) {
908
+ const utilsImport = sourceFile.getImportDeclaration(
909
+ (decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
809
910
  );
911
+ if (utilsImport) {
912
+ const namedImports = utilsImport.getNamedImports();
913
+ if (!namedImports.some((imp) => imp.getName() === "cn")) {
914
+ utilsImport.addNamedImport("cn");
915
+ }
916
+ } else {
917
+ sourceFile.addImportDeclaration({
918
+ moduleSpecifier: "@/lib/utils",
919
+ namedImports: ["cn"]
920
+ });
921
+ }
810
922
  }
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
923
  }
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)}`);
924
+ function parseTemplateLiteralToCnArgs(templateLiteral) {
925
+ const staticArgs = [];
926
+ const variableArgs = [];
927
+ const content = templateLiteral.slice(1, -1);
928
+ const parts = content.split(/(\$\{[^}]+\})/);
929
+ for (const part of parts) {
930
+ if (!part) continue;
931
+ if (part.startsWith("${") && part.endsWith("}")) {
932
+ const expr = part.slice(2, -1).trim();
933
+ if (expr) variableArgs.push(expr);
934
+ } else {
935
+ const staticParts = part.trim().split(/\s+/).filter(Boolean);
936
+ for (const staticPart of staticParts) {
937
+ staticArgs.push(`"${staticPart}"`);
938
+ }
851
939
  }
852
- removeCssImport(cssPath, "./fonts.css");
853
940
  }
854
- return true;
941
+ return [...staticArgs, ...variableArgs];
855
942
  }
856
943
 
857
944
  // src/commands/init.ts
858
- function buildConfig(project, fontChoice) {
945
+ var DEFAULT_FONT_NAMES = ["font-geist", "font-geist-mono"];
946
+ function buildConfig(project, strategy) {
859
947
  let cssPath;
860
948
  if (project.existingCssFile) {
861
949
  cssPath = project.existingCssFile;
@@ -876,8 +964,7 @@ function buildConfig(project, fontChoice) {
876
964
  hooks: "@/hooks"
877
965
  },
878
966
  fonts: {
879
- strategy: fontChoice.strategy,
880
- ...fontChoice.useNextFont ? { nextFont: true } : {}
967
+ strategy
881
968
  }
882
969
  };
883
970
  }
@@ -885,7 +972,7 @@ function stripTailwindCoreImport(content) {
885
972
  return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
886
973
  }
887
974
  function insertImportLine(filePath, importStatement) {
888
- const content = fs11.readFileSync(filePath, "utf-8");
975
+ const content = fs9.readFileSync(filePath, "utf-8");
889
976
  const lines = content.split("\n");
890
977
  let lastImportIndex = -1;
891
978
  for (let i = 0; i < lines.length; i++) {
@@ -898,17 +985,12 @@ function insertImportLine(filePath, importStatement) {
898
985
  } else {
899
986
  lines.unshift(importStatement);
900
987
  }
901
- fs11.writeFileSync(filePath, lines.join("\n"), "utf-8");
988
+ fs9.writeFileSync(filePath, lines.join("\n"), "utf-8");
902
989
  }
903
- async function promptFontChoice(project, detected, skipPrompts) {
990
+ async function promptFontStrategy(detected, skipPrompts) {
904
991
  const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
905
992
  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
- }
993
+ if (skipPrompts) return defaultStrategy;
912
994
  if (hasExistingFonts) {
913
995
  const sources = [];
914
996
  if (detected.fontFamilies.length > 0) {
@@ -930,7 +1012,7 @@ async function promptFontChoice(project, detected, skipPrompts) {
930
1012
  choices.push({
931
1013
  value: "stera-default",
932
1014
  name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
933
- description: "Self-hosted, optimized variable fonts"
1015
+ description: "Optimized variable fonts via next/font or fontsource"
934
1016
  });
935
1017
  choices.push({
936
1018
  value: "skip",
@@ -942,56 +1024,79 @@ async function promptFontChoice(project, detected, skipPrompts) {
942
1024
  choices,
943
1025
  default: defaultStrategy
944
1026
  });
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
1027
  console.log("");
953
- return { strategy, useNextFont };
1028
+ return strategy;
954
1029
  }
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)) {
1030
+ async function fetchDefaultFonts() {
1031
+ const fonts = [];
1032
+ for (const name of DEFAULT_FONT_NAMES) {
1033
+ try {
1034
+ const item = await fetchRegistryItem(name);
1035
+ if (item.type === "registry:font") {
1036
+ fonts.push(item);
1037
+ }
1038
+ } catch {
1039
+ }
1040
+ }
1041
+ return fonts;
1042
+ }
1043
+ async function executeFontStrategy(strategy, project, detected, config, cwd) {
1044
+ if (strategy === "skip") return;
1045
+ if (strategy === "keep-existing") {
1046
+ const cssPath = path10.resolve(cwd, config.css);
1047
+ const cssDir = path10.dirname(cssPath);
1048
+ if (fs9.existsSync(cssPath)) {
963
1049
  const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
964
1050
  const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
965
1051
  const patched = patchCssVariables(cssPath, {
966
- "--font-display": fontValue,
967
- "--font-body": fontValue
1052
+ "--font-heading": fontValue,
1053
+ "--font-sans": fontValue
968
1054
  });
969
- const steraUiPath = path12.join(cssDir, "stera-ui.css");
970
- if (fs11.existsSync(steraUiPath)) {
1055
+ const steraUiPath = path10.join(cssDir, "stera-ui.css");
1056
+ if (fs9.existsSync(steraUiPath)) {
971
1057
  patchCssVariables(steraUiPath, {
972
- "--font-display": fontValue,
973
- "--font-body": fontValue
1058
+ "--font-heading": fontValue,
1059
+ "--font-sans": fontValue
974
1060
  });
975
1061
  }
976
1062
  if (patched.length > 0) {
977
1063
  console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
978
1064
  }
979
1065
  }
980
- return {};
1066
+ return;
981
1067
  }
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 };
1068
+ const fonts = await fetchDefaultFonts();
1069
+ if (fonts.length === 0) return;
1070
+ const fontTree = massageTreeForFonts(fonts, project);
1071
+ if (fontTree.dependencies.length > 0) {
1072
+ await installDependencies(fontTree.dependencies, cwd);
1073
+ }
1074
+ if (fontTree.cssImports.length > 0) {
1075
+ const cssPath = path10.resolve(cwd, config.css);
1076
+ const cssDir = path10.dirname(cssPath);
1077
+ const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
1078
+ if (fs9.existsSync(targetCss)) {
1079
+ for (const importLine of fontTree.cssImports) {
1080
+ insertImportLine(targetCss, importLine);
1081
+ }
1082
+ console.log(` ${CHECK} Added font imports to ${path10.relative(cwd, targetCss)}`);
1083
+ }
1084
+ }
1085
+ if (Object.keys(fontTree.cssVars).length > 0) {
1086
+ const cssPath = path10.resolve(cwd, config.css);
1087
+ const cssDir = path10.dirname(cssPath);
1088
+ const targetCss = fs9.existsSync(path10.join(cssDir, "stera-ui.css")) ? path10.join(cssDir, "stera-ui.css") : cssPath;
1089
+ if (fs9.existsSync(targetCss)) {
1090
+ const patched = patchCssVariables(targetCss, fontTree.cssVars);
1091
+ if (patched.length > 0) {
1092
+ console.log(` ${CHECK} Updated font variables in ${path10.relative(cwd, targetCss)}`);
1093
+ }
1094
+ }
991
1095
  }
1096
+ await updateFonts(fonts, project, cwd);
992
1097
  }
993
1098
  async function init(options) {
994
- const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
1099
+ const cwd = options.cwd ? path10.resolve(options.cwd) : process.cwd();
995
1100
  console.log(`
996
1101
  ${LOGO} Stera UI
997
1102
  `);
@@ -1001,8 +1106,8 @@ async function init(options) {
1001
1106
  console.log(' Run "stera-ui add <component>" to add components.\n');
1002
1107
  return;
1003
1108
  }
1004
- const pkgPath = path12.join(cwd, "package.json");
1005
- if (!fs11.existsSync(pkgPath)) {
1109
+ const pkgPath = path10.join(cwd, "package.json");
1110
+ if (!fs9.existsSync(pkgPath)) {
1006
1111
  console.error(
1007
1112
  " Error: No package.json found. Please run this in a project directory."
1008
1113
  );
@@ -1018,35 +1123,34 @@ async function init(options) {
1018
1123
  `);
1019
1124
  }
1020
1125
  const detectedFonts = detectFonts(cwd, project);
1021
- const fontChoice = await promptFontChoice(
1022
- project,
1126
+ const strategy = await promptFontStrategy(
1023
1127
  detectedFonts,
1024
1128
  options.yes ?? false
1025
1129
  );
1026
- const config = buildConfig(project, fontChoice);
1027
- const configPath = path12.join(cwd, CONFIG_FILE);
1028
- fs11.writeFileSync(
1130
+ const config = buildConfig(project, strategy);
1131
+ const configPath = path10.join(cwd, CONFIG_FILE);
1132
+ fs9.writeFileSync(
1029
1133
  configPath,
1030
1134
  JSON.stringify(config, null, 2) + "\n",
1031
1135
  "utf-8"
1032
1136
  );
1033
1137
  console.log(` ${CHECK} Created components.json`);
1034
- const cssDir = path12.dirname(path12.resolve(cwd, config.css));
1138
+ const cssDir = path10.dirname(path10.resolve(cwd, config.css));
1035
1139
  if (project.existingCssFile) {
1036
1140
  if (hasGlobalsCss(config, cwd)) {
1037
1141
  console.log(
1038
- ` ${CHECK} Stera already initialised ${dim(`(${path12.join(path12.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
1142
+ ` ${CHECK} Stera already initialised ${dim(`(${path10.join(path10.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
1039
1143
  );
1040
1144
  } else {
1041
1145
  try {
1042
1146
  const globalsItem = await fetchRegistryItem("globals");
1043
- const globalsContent = globalsItem.files[0].content ?? "";
1147
+ const globalsContent = globalsItem.files?.[0]?.content ?? "";
1044
1148
  const steraUiContent = stripTailwindCoreImport(globalsContent);
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);
1149
+ const steraUiPath = path10.join(cssDir, "stera-ui.css");
1150
+ fs9.mkdirSync(cssDir, { recursive: true });
1151
+ fs9.writeFileSync(steraUiPath, steraUiContent, "utf-8");
1152
+ console.log(` ${CHECK} Created ${path10.relative(cwd, steraUiPath)}`);
1153
+ const existingCssPath = path10.resolve(cwd, project.existingCssFile);
1050
1154
  insertImportLine(existingCssPath, "@import './stera-ui.css';");
1051
1155
  console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
1052
1156
  } catch {
@@ -1054,13 +1158,14 @@ async function init(options) {
1054
1158
  }
1055
1159
  } else {
1056
1160
  const resolved = await resolveDependencies(["globals"]);
1057
- const { written } = await writeComponentFiles(resolved, config, cwd);
1161
+ const filtered = resolved.filter((item) => item.name !== "fonts");
1162
+ const { written } = await writeComponentFiles(filtered, config, cwd);
1058
1163
  for (const file of written) {
1059
1164
  console.log(` ${CHECK} Created ${file}`);
1060
1165
  }
1061
1166
  }
1062
- const fontResult = await executeFontStrategy(
1063
- fontChoice,
1167
+ await executeFontStrategy(
1168
+ strategy,
1064
1169
  project,
1065
1170
  detectedFonts,
1066
1171
  config,
@@ -1078,15 +1183,7 @@ async function init(options) {
1078
1183
  console.log("");
1079
1184
  console.log(" Next steps:");
1080
1185
  console.log(" stera-ui add <component>");
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") {
1186
+ if (strategy === "skip") {
1090
1187
  console.log("");
1091
1188
  console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
1092
1189
  }