stera-ui 0.2.1 → 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)) {
@@ -401,7 +401,7 @@ async function add(components, options) {
401
401
  }
402
402
  const resolved = await resolveDependencies(components);
403
403
  for (const item of resolved) {
404
- for (const file of item.files) {
404
+ for (const file of item.files ?? []) {
405
405
  if (file.content) {
406
406
  file.content = applyTransforms(
407
407
  file.content,
@@ -451,9 +451,9 @@ async function add(components, options) {
451
451
  }
452
452
 
453
453
  // src/commands/init.ts
454
- import fs11 from "fs";
455
- import path12 from "path";
456
- import { select, confirm } from "@inquirer/prompts";
454
+ import fs9 from "fs";
455
+ import path10 from "path";
456
+ import { select } from "@inquirer/prompts";
457
457
 
458
458
  // src/utils/detect-project.ts
459
459
  import fs5 from "fs";
@@ -626,243 +626,324 @@ function patchCssVariables(filePath, patches) {
626
626
  }
627
627
  return patched;
628
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
629
  function escapeRegex(str) {
639
630
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
640
631
  }
641
632
 
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";
633
+ // src/utils/update-fonts.ts
634
+ import { existsSync, promises as fs8 } from "fs";
648
635
  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");
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;
673
667
  }
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?`
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
687
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("--", ""));
688
760
  }
689
- fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
690
- fs8.copyFileSync(srcPath, destPath);
691
- written.push(path9.relative(cwd, destPath));
692
761
  }
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");
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);
697
772
  }
698
- return written;
773
+ return sourceFile.getFullText();
699
774
  }
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;
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, "'");
717
781
  }
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;
782
+ function isRootFontVariable(variable) {
783
+ return ROOT_FONT_VARIABLES.has(variable);
725
784
  }
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;
785
+ function shouldApplyFontUtilityToHtml(font) {
786
+ return !font.font.selector && isRootFontVariable(font.font.variable);
734
787
  }
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;
788
+ function getFontVariableName(importName, variable) {
789
+ const baseName = toCamelCase(importName);
790
+ if (isRootFontVariable(variable)) return baseName;
791
+ return `${baseName}${toPascalCase(variable.replace(/^--font-/, ""))}`;
742
792
  }
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 };
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("");
757
797
  }
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;
798
+ function toPascalCase(str) {
799
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
768
800
  }
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;
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
+ }
776
813
  }
777
814
  }
778
- if (lastImportIndex >= 0) {
779
- lines.splice(lastImportIndex + 1, 0, imports);
780
- } else {
781
- lines.unshift(imports);
782
- }
783
- return lines.join("\n");
815
+ return null;
784
816
  }
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
- );
817
+ function findInsertPosition(sourceFile) {
818
+ const imports = sourceFile.getImportDeclarations();
819
+ if (imports.length > 0) {
820
+ return imports[imports.length - 1].getChildIndex() + 1;
797
821
  }
798
- const exprMatch = content.match(
799
- /(<html[^>]*className=\{)([^}`]+)(\})/
822
+ return 0;
823
+ }
824
+ function updateHtmlClassName(sourceFile, fontVariableNames, fontUtilityClasses) {
825
+ const jsxElements = sourceFile.getDescendantsOfKind(
826
+ SyntaxKind.JsxOpeningElement
800
827
  );
801
- if (exprMatch) {
802
- return content.replace(
803
- exprMatch[0],
804
- `${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`
805
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
+ }
806
901
  }
807
- const stringMatch = content.match(
808
- /(<html[^>]*className=)(["'])([^"']*)(["'])/
902
+ }
903
+ function ensureCnImport(sourceFile) {
904
+ const existingImport = sourceFile.getImportDeclaration(
905
+ (decl) => decl.getNamedImports().some((imp) => imp.getName() === "cn")
809
906
  );
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}\`}`
907
+ if (!existingImport) {
908
+ const utilsImport = sourceFile.getImportDeclaration(
909
+ (decl) => decl.getModuleSpecifierValue().includes("/lib/utils")
816
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
+ }
817
922
  }
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
923
  }
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)}`);
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
+ }
858
939
  }
859
- removeCssImport(cssPath, "./fonts.css");
860
940
  }
861
- return true;
941
+ return [...staticArgs, ...variableArgs];
862
942
  }
863
943
 
864
944
  // src/commands/init.ts
865
- function buildConfig(project, fontChoice) {
945
+ var DEFAULT_FONT_NAMES = ["font-geist", "font-geist-mono"];
946
+ function buildConfig(project, strategy) {
866
947
  let cssPath;
867
948
  if (project.existingCssFile) {
868
949
  cssPath = project.existingCssFile;
@@ -883,8 +964,7 @@ function buildConfig(project, fontChoice) {
883
964
  hooks: "@/hooks"
884
965
  },
885
966
  fonts: {
886
- strategy: fontChoice.strategy,
887
- ...fontChoice.useNextFont ? { nextFont: true } : {}
967
+ strategy
888
968
  }
889
969
  };
890
970
  }
@@ -892,7 +972,7 @@ function stripTailwindCoreImport(content) {
892
972
  return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
893
973
  }
894
974
  function insertImportLine(filePath, importStatement) {
895
- const content = fs11.readFileSync(filePath, "utf-8");
975
+ const content = fs9.readFileSync(filePath, "utf-8");
896
976
  const lines = content.split("\n");
897
977
  let lastImportIndex = -1;
898
978
  for (let i = 0; i < lines.length; i++) {
@@ -905,17 +985,12 @@ function insertImportLine(filePath, importStatement) {
905
985
  } else {
906
986
  lines.unshift(importStatement);
907
987
  }
908
- fs11.writeFileSync(filePath, lines.join("\n"), "utf-8");
988
+ fs9.writeFileSync(filePath, lines.join("\n"), "utf-8");
909
989
  }
910
- async function promptFontChoice(project, detected, skipPrompts) {
990
+ async function promptFontStrategy(detected, skipPrompts) {
911
991
  const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
912
992
  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
- }
993
+ if (skipPrompts) return defaultStrategy;
919
994
  if (hasExistingFonts) {
920
995
  const sources = [];
921
996
  if (detected.fontFamilies.length > 0) {
@@ -937,7 +1012,7 @@ async function promptFontChoice(project, detected, skipPrompts) {
937
1012
  choices.push({
938
1013
  value: "stera-default",
939
1014
  name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
940
- description: "Self-hosted, optimized variable fonts"
1015
+ description: "Optimized variable fonts via next/font or fontsource"
941
1016
  });
942
1017
  choices.push({
943
1018
  value: "skip",
@@ -949,56 +1024,79 @@ async function promptFontChoice(project, detected, skipPrompts) {
949
1024
  choices,
950
1025
  default: defaultStrategy
951
1026
  });
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
1027
  console.log("");
960
- return { strategy, useNextFont };
1028
+ return strategy;
961
1029
  }
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)) {
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)) {
970
1049
  const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
971
1050
  const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
972
1051
  const patched = patchCssVariables(cssPath, {
973
- "--font-display": fontValue,
974
- "--font-body": fontValue
1052
+ "--font-heading": fontValue,
1053
+ "--font-sans": fontValue
975
1054
  });
976
- const steraUiPath = path12.join(cssDir, "stera-ui.css");
977
- if (fs11.existsSync(steraUiPath)) {
1055
+ const steraUiPath = path10.join(cssDir, "stera-ui.css");
1056
+ if (fs9.existsSync(steraUiPath)) {
978
1057
  patchCssVariables(steraUiPath, {
979
- "--font-display": fontValue,
980
- "--font-body": fontValue
1058
+ "--font-heading": fontValue,
1059
+ "--font-sans": fontValue
981
1060
  });
982
1061
  }
983
1062
  if (patched.length > 0) {
984
1063
  console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
985
1064
  }
986
1065
  }
987
- return {};
1066
+ return;
988
1067
  }
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 };
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
+ }
998
1095
  }
1096
+ await updateFonts(fonts, project, cwd);
999
1097
  }
1000
1098
  async function init(options) {
1001
- const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
1099
+ const cwd = options.cwd ? path10.resolve(options.cwd) : process.cwd();
1002
1100
  console.log(`
1003
1101
  ${LOGO} Stera UI
1004
1102
  `);
@@ -1008,8 +1106,8 @@ async function init(options) {
1008
1106
  console.log(' Run "stera-ui add <component>" to add components.\n');
1009
1107
  return;
1010
1108
  }
1011
- const pkgPath = path12.join(cwd, "package.json");
1012
- if (!fs11.existsSync(pkgPath)) {
1109
+ const pkgPath = path10.join(cwd, "package.json");
1110
+ if (!fs9.existsSync(pkgPath)) {
1013
1111
  console.error(
1014
1112
  " Error: No package.json found. Please run this in a project directory."
1015
1113
  );
@@ -1025,35 +1123,34 @@ async function init(options) {
1025
1123
  `);
1026
1124
  }
1027
1125
  const detectedFonts = detectFonts(cwd, project);
1028
- const fontChoice = await promptFontChoice(
1029
- project,
1126
+ const strategy = await promptFontStrategy(
1030
1127
  detectedFonts,
1031
1128
  options.yes ?? false
1032
1129
  );
1033
- const config = buildConfig(project, fontChoice);
1034
- const configPath = path12.join(cwd, CONFIG_FILE);
1035
- fs11.writeFileSync(
1130
+ const config = buildConfig(project, strategy);
1131
+ const configPath = path10.join(cwd, CONFIG_FILE);
1132
+ fs9.writeFileSync(
1036
1133
  configPath,
1037
1134
  JSON.stringify(config, null, 2) + "\n",
1038
1135
  "utf-8"
1039
1136
  );
1040
1137
  console.log(` ${CHECK} Created components.json`);
1041
- const cssDir = path12.dirname(path12.resolve(cwd, config.css));
1138
+ const cssDir = path10.dirname(path10.resolve(cwd, config.css));
1042
1139
  if (project.existingCssFile) {
1043
1140
  if (hasGlobalsCss(config, cwd)) {
1044
1141
  console.log(
1045
- ` ${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)`)}`
1046
1143
  );
1047
1144
  } else {
1048
1145
  try {
1049
1146
  const globalsItem = await fetchRegistryItem("globals");
1050
- const globalsContent = globalsItem.files[0].content ?? "";
1147
+ const globalsContent = globalsItem.files?.[0]?.content ?? "";
1051
1148
  const steraUiContent = stripTailwindCoreImport(globalsContent);
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);
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);
1057
1154
  insertImportLine(existingCssPath, "@import './stera-ui.css';");
1058
1155
  console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
1059
1156
  } catch {
@@ -1061,13 +1158,14 @@ async function init(options) {
1061
1158
  }
1062
1159
  } else {
1063
1160
  const resolved = await resolveDependencies(["globals"]);
1064
- const { written } = await writeComponentFiles(resolved, config, cwd);
1161
+ const filtered = resolved.filter((item) => item.name !== "fonts");
1162
+ const { written } = await writeComponentFiles(filtered, config, cwd);
1065
1163
  for (const file of written) {
1066
1164
  console.log(` ${CHECK} Created ${file}`);
1067
1165
  }
1068
1166
  }
1069
- const fontResult = await executeFontStrategy(
1070
- fontChoice,
1167
+ await executeFontStrategy(
1168
+ strategy,
1071
1169
  project,
1072
1170
  detectedFonts,
1073
1171
  config,
@@ -1085,15 +1183,7 @@ async function init(options) {
1085
1183
  console.log("");
1086
1184
  console.log(" Next steps:");
1087
1185
  console.log(" stera-ui add <component>");
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") {
1186
+ if (strategy === "skip") {
1097
1187
  console.log("");
1098
1188
  console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
1099
1189
  }