@upstart.gg/vite-plugins 0.1.78 → 0.1.80

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.
@@ -1,5 +1,5 @@
1
1
  import * as _$unplugin from "unplugin";
2
- import { Theme } from "@upstart.gg/sdk/themes/theme";
2
+ import { GoogleFont, Theme } from "@upstart.gg/sdk/themes/theme";
3
3
 
4
4
  //#region src/vite-plugin-upstart-theme.d.ts
5
5
  interface PluginOptions {
@@ -14,11 +14,25 @@ interface PluginOptions {
14
14
  /** CSS framework to target (default: 'daisy-ui') */
15
15
  cssFramework?: "daisy-ui" | "shadcn-ui";
16
16
  }
17
+ /**
18
+ * Builds the `family=` parameter of a Google Fonts css2 URL for one font.
19
+ *
20
+ * Weights are requested as a discrete list (`wght@300;400;500`), never as a range
21
+ * (`wght@300..700`). Google drops a family from a multi-family request when a range falls outside
22
+ * its weight axis — single-weight display fonts (Anton, Bebas Neue, Instrument Serif…) and fonts
23
+ * whose axis starts at 400 (Playfair Display) — and the site silently falls back to a system font.
24
+ * A discrete list is tolerated: Google serves the weights the family has and skips the others.
25
+ */
26
+ declare function googleFontFamilyParam(font: Pick<GoogleFont, "family" | "weights">): string;
27
+ /**
28
+ * Generates the fonts.css file content with Google Fonts imports
29
+ */
30
+ declare function generateFontsCSS(lightTheme: Theme, darkTheme?: Theme | null, display?: "auto" | "block" | "swap" | "fallback" | "optional"): string;
17
31
  /**
18
32
  * Unplugin-based theme generator
19
33
  */
20
34
  declare const upstartTheme: _$unplugin.UnpluginInstance<PluginOptions, boolean>;
21
35
  declare const _default: (options: PluginOptions) => _$unplugin.VitePlugin<any>[] | _$unplugin.VitePlugin<any>;
22
36
  //#endregion
23
- export { _default as default, upstartTheme };
37
+ export { _default as default, generateFontsCSS, googleFontFamilyParam, upstartTheme };
24
38
  //# sourceMappingURL=vite-plugin-upstart-theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-upstart-theme.d.ts","names":[],"sources":["../src/vite-plugin-upstart-theme.ts"],"mappings":";;;;UAWU,aAAA;EACR,MAAA;IACE,KAAA,EAAO,KAAA;IACP,IAAA,GAAO,KAAA;IACP,OAAA;EAAA;EAEF,UAAA;EALA;EAOA,YAAA;EANS;EAQT,YAAA;AAAA;;;;cA+VW,YAAA,EAAY,UAAA,CAAA,gBAAA,CAAA,aAAA;AAAA,cAoEtB,QAAA"}
1
+ {"version":3,"file":"vite-plugin-upstart-theme.d.ts","names":[],"sources":["../src/vite-plugin-upstart-theme.ts"],"mappings":";;;;UAWU,aAAA;EACR,MAAA;IACE,KAAA,EAAO,KAAA;IACP,IAAA,GAAO,KAAA;IACP,OAAA;EAAA;EAEF,UAAA;EALA;EAOA,YAAA;EANS;EAQT,YAAA;AAAA;;;;;;;AAsCF;;;iBAAgB,qBAAA,CAAsB,IAAA,EAAM,IAAA,CAAK,UAAA;;;;iBAiBjC,gBAAA,CACd,UAAA,EAAY,KAAA,EACZ,SAAA,GAAW,KAAA,SACX,OAAA;;AAHF;;cAsTa,YAAA,EAAY,UAAA,CAAA,gBAAA,CAAA,aAAA;AAAA,cAoEtB,QAAA"}
@@ -23,6 +23,27 @@ function collectGoogleFonts(theme) {
23
23
  return googleFonts;
24
24
  }
25
25
  /**
26
+ * Builds the `family=` parameter of a Google Fonts css2 URL for one font.
27
+ *
28
+ * Weights are requested as a discrete list (`wght@300;400;500`), never as a range
29
+ * (`wght@300..700`). Google drops a family from a multi-family request when a range falls outside
30
+ * its weight axis — single-weight display fonts (Anton, Bebas Neue, Instrument Serif…) and fonts
31
+ * whose axis starts at 400 (Playfair Display) — and the site silently falls back to a system font.
32
+ * A discrete list is tolerated: Google serves the weights the family has and skips the others.
33
+ */
34
+ function googleFontFamilyParam(font) {
35
+ const name = font.family.replace(/ /g, "+");
36
+ if (!font.weights?.length) return `family=${name}`;
37
+ const minWeight = Math.min(...font.weights);
38
+ const maxWeight = Math.max(...font.weights);
39
+ if (minWeight === maxWeight) return `family=${name}:wght@${minWeight}`;
40
+ const weights = [];
41
+ for (let w = Math.ceil(minWeight / 100) * 100; w <= maxWeight; w += 100) weights.push(w);
42
+ if (weights[0] !== minWeight) weights.unshift(minWeight);
43
+ if (weights.at(-1) !== maxWeight) weights.push(maxWeight);
44
+ return `family=${name}:wght@${weights.join(";")}`;
45
+ }
46
+ /**
26
47
  * Generates the fonts.css file content with Google Fonts imports
27
48
  */
28
49
  function generateFontsCSS(lightTheme, darkTheme = null, display = "swap") {
@@ -30,11 +51,7 @@ function generateFontsCSS(lightTheme, darkTheme = null, display = "swap") {
30
51
  for (const font of collectGoogleFonts(lightTheme)) allFonts.set(font.family, font);
31
52
  if (darkTheme) for (const font of collectGoogleFonts(darkTheme)) allFonts.set(font.family, font);
32
53
  if (allFonts.size === 0) return "/* No Google Fonts to import */\n";
33
- return `/* Google Fonts - Auto-generated by vite-plugin-upstart-theme */\n@import url("${`https://fonts.googleapis.com/css2?${Array.from(allFonts.values()).map((font) => {
34
- const name = font.family.replace(/ /g, "+");
35
- if (font.weights && font.weights.length > 1 && font.weights[0] !== font.weights.at(-1)) return `family=${name}:wght@${Math.min(...font.weights)}..${Math.max(...font.weights)}`;
36
- return `family=${name}`;
37
- }).join("&")}&display=${display}`}");\n`;
54
+ return `/* Google Fonts - Auto-generated by vite-plugin-upstart-theme */\n@import url("${`https://fonts.googleapis.com/css2?${Array.from(allFonts.values()).map(googleFontFamilyParam).join("&")}&display=${display}`}");\n`;
38
55
  }
39
56
  /**
40
57
  * Converts a font definition to a CSS font-family string
@@ -281,6 +298,6 @@ const upstartTheme = createUnplugin((opts) => {
281
298
  });
282
299
  var vite_plugin_upstart_theme_default = upstartTheme.vite;
283
300
  //#endregion
284
- export { vite_plugin_upstart_theme_default as default, upstartTheme };
301
+ export { vite_plugin_upstart_theme_default as default, generateFontsCSS, googleFontFamilyParam, upstartTheme };
285
302
 
286
303
  //# sourceMappingURL=vite-plugin-upstart-theme.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-upstart-theme.js","names":[],"sources":["../src/vite-plugin-upstart-theme.ts"],"sourcesContent":["import { createUnplugin } from \"unplugin\";\nimport {\n GenericFont,\n GoogleFont,\n type Theme,\n fontStacksFonts,\n processTheme,\n} from \"@upstart.gg/sdk/themes/theme\";\nimport fs from \"fs\";\nimport path from \"path\";\n\ninterface PluginOptions {\n themes: {\n light: Theme;\n dark?: Theme;\n default: \"light\" | \"dark\";\n };\n outputPath: string;\n /** Google Fonts display parameter (default: 'swap') */\n fontsDisplay?: \"auto\" | \"block\" | \"swap\" | \"fallback\" | \"optional\";\n /** CSS framework to target (default: 'daisy-ui') */\n cssFramework?: \"daisy-ui\" | \"shadcn-ui\";\n}\n\n/**\n * Collects Google Fonts from a theme's typography configuration\n */\nfunction collectGoogleFonts(theme: Theme) {\n const { typography } = theme;\n if (!typography) return [];\n\n const googleFonts: Array<GoogleFont> = [];\n const seen = new Set<string>();\n\n const addFont = (font?: GenericFont) => {\n if (font?.type === \"google\" && !seen.has(font.family)) {\n seen.add(font.family);\n googleFonts.push(font);\n }\n };\n\n addFont(typography.sans);\n addFont(typography.serif);\n if (typography.others) {\n Object.values(typography.others).forEach(addFont);\n }\n\n return googleFonts;\n}\n\n/**\n * Generates the fonts.css file content with Google Fonts imports\n */\nfunction generateFontsCSS(\n lightTheme: Theme,\n darkTheme: Theme | null = null,\n display: \"auto\" | \"block\" | \"swap\" | \"fallback\" | \"optional\" = \"swap\",\n): string {\n // Collect fonts from both themes\n const allFonts = new Map<string, GoogleFont>();\n\n for (const font of collectGoogleFonts(lightTheme)) {\n allFonts.set(font.family, font);\n }\n\n if (darkTheme) {\n for (const font of collectGoogleFonts(darkTheme)) {\n allFonts.set(font.family, font);\n }\n }\n\n if (allFonts.size === 0) {\n return \"/* No Google Fonts to import */\\n\";\n }\n\n // Build the Google Fonts URL\n const families = Array.from(allFonts.values()).map((font) => {\n const name = font.family.replace(/ /g, \"+\");\n if (font.weights && font.weights.length > 1 && font.weights[0] !== font.weights.at(-1)) {\n // Take min weight and max weight\n const minWeight = Math.min(...font.weights);\n const maxWeight = Math.max(...font.weights);\n return `family=${name}:wght@${minWeight}..${maxWeight}`;\n }\n return `family=${name}`;\n });\n\n const url = `https://fonts.googleapis.com/css2?${families.join(\"&\")}&display=${display}`;\n\n return `/* Google Fonts - Auto-generated by vite-plugin-upstart-theme */\\n@import url(\"${url}\");\\n`;\n}\n\n/**\n * Converts a font definition to a CSS font-family string\n */\nfunction fontToCss(font: Theme[\"typography\"][\"sans\"]): string {\n if (!font) return \"\";\n\n if (font.type === \"stack\") {\n return fontStacksFonts[font.family];\n }\n\n if (font.type === \"google\") {\n // For Google fonts, we just use the family name\n // The actual font loading will be handled separately\n return `\"${font.family}\", system-ui, sans-serif`;\n }\n\n return \"\";\n}\n\n/**\n * Converts camelCase to kebab-case\n */\nfunction camelToKebab(str: string): string {\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\n/**\n * Generates @theme block with font definitions\n */\nfunction generateThemeBlock(\n lightTheme: Theme,\n darkTheme: Theme | null = null,\n defaultTheme: \"light\" | \"dark\" = \"light\",\n): string {\n const fontDefinitions = new Map<string, string>();\n\n // Process light theme fonts\n const lightTypography = lightTheme.typography;\n\n // Add sans font\n if (lightTypography.sans) {\n const fontCss = fontToCss(lightTypography.sans);\n if (fontCss) {\n fontDefinitions.set(\"sans\", fontCss);\n }\n }\n\n // Add serif font\n if (lightTypography.serif) {\n const fontCss = fontToCss(lightTypography.serif);\n if (fontCss) {\n fontDefinitions.set(\"serif\", fontCss);\n }\n }\n\n // Add other fonts\n if (lightTypography.others) {\n for (const [name, font] of Object.entries(lightTypography.others)) {\n const fontCss = fontToCss(font);\n if (fontCss) {\n fontDefinitions.set(name, fontCss);\n }\n }\n }\n\n // Process dark theme fonts if provided\n if (darkTheme) {\n const darkTypography = darkTheme.typography;\n\n if (darkTypography.sans) {\n const fontCss = fontToCss(darkTypography.sans);\n if (fontCss) {\n fontDefinitions.set(\"sans\", fontCss);\n }\n }\n\n if (darkTypography.serif) {\n const fontCss = fontToCss(darkTypography.serif);\n if (fontCss) {\n fontDefinitions.set(\"serif\", fontCss);\n }\n }\n\n if (darkTypography.others) {\n for (const [name, font] of Object.entries(darkTypography.others)) {\n const fontCss = fontToCss(font);\n if (fontCss) {\n fontDefinitions.set(name, fontCss);\n }\n }\n }\n }\n\n const fontLines = Array.from(fontDefinitions.entries())\n .map(([name, value]) => ` --font-${name}: ${value};`)\n .join(\"\\n\");\n\n return `/* Fonts in the tailwind @theme block */\n@theme {\n ${fontLines}\n}\n\n/* Exclude properties since it produce warnings and is not important */\n@plugin \"daisyui\" {\n exclude: properties;\n themes: ${lightTheme.id} ${defaultTheme === \"light\" || !darkTheme ? \"--default\" : \"\"}${darkTheme?.id && darkTheme?.id !== lightTheme.id ? `, ${darkTheme.id} ${defaultTheme === \"dark\" ? \"--default\" : \"--prefersdark\"}` : \"\"};\n logs: true;\n}\n`;\n}\n\n/**\n * Generates @theme block for a single theme with colors and layout variables\n */\nfunction generateThemeVariables(theme: Theme, defaultTheme: \"light\" | \"dark\"): string {\n const processed = processTheme(theme);\n const lines: string[] = [\n ` name: \"${processed.id}\";`,\n ` color-scheme: ${processed.browserColorScheme};`,\n ...((defaultTheme === \"dark\" && theme.browserColorScheme === \"dark\") ||\n (defaultTheme === \"light\" && theme.browserColorScheme === \"light\")\n ? [` default: true;`]\n : []),\n // ...(type === \"dark\" && defaultTheme !== \"dark\" ? [` prefersdark: true;`] : [` prefersdark: false;`]),\n ];\n\n // Color variables\n for (const [colorKey, colorValue] of Object.entries(processed.colors)) {\n const kebabKey = [\"base100\", \"base200\", \"base300\"].includes(colorKey)\n ? colorKey.replace(\"base\", \"base-\")\n : camelToKebab(colorKey);\n lines.push(` --color-${kebabKey}: ${colorValue};`);\n }\n\n lines.push(\"\");\n\n // Layout variables\n lines.push(` --radius-selector: ${processed.radiusSelector};`);\n lines.push(` --radius-field: ${processed.radiusField};`);\n lines.push(` --radius-box: ${processed.radiusBox};`);\n lines.push(` --size-selector: ${processed.sizeSelector};`);\n lines.push(` --size-field: ${processed.sizeField};`);\n lines.push(` --border: ${processed.border};`);\n lines.push(` --depth: ${processed.depth ? 1 : 0};`);\n lines.push(` --noise: 0;`);\n\n // return the generated lines\n return lines.join(\"\\n\");\n}\n\n/**\n * Builds the CSS variable block for a single shadcn theme (`:root` or `.dark`)\n */\nfunction buildShadcnVars(theme: Theme): string {\n const processed = processTheme(theme);\n const { colors } = processed;\n\n const monoFont = theme.typography?.others?.mono;\n const monoFamilyValue = monoFont ? fontToCss(monoFont) : \"ui-monospace, monospace\";\n const sansFamilyValue = theme.typography?.sans\n ? fontToCss(theme.typography.sans)\n : \"ui-sans-serif, system-ui, sans-serif\";\n\n const vars: [string, string][] = [\n [\"--radius\", processed.radiusField],\n [\"--background\", colors.base100],\n [\"--foreground\", colors.baseContent],\n [\"--card\", colors.base200],\n [\"--card-foreground\", colors.baseContent],\n [\"--popover\", colors.base200],\n [\"--popover-foreground\", colors.baseContent],\n [\"--primary\", colors.primary],\n [\"--primary-foreground\", colors.primaryContent],\n [\"--secondary\", colors.secondary],\n [\"--secondary-foreground\", colors.secondaryContent],\n [\"--muted\", colors.neutral],\n [\"--muted-foreground\", colors.neutralContent],\n // shadcn --accent = hover surface, NOT the vibrant accent color\n [\"--accent\", colors.base300],\n [\"--accent-foreground\", colors.baseContent],\n [\"--destructive\", colors.error],\n [\"--border\", colors.base300],\n [\"--input\", colors.base300],\n [\"--ring\", colors.primary],\n [\"--chart-1\", colors.primary],\n [\"--chart-2\", colors.secondary],\n // vibrant accent goes in chart-3\n [\"--chart-3\", colors.accent],\n [\"--chart-4\", colors.success],\n [\"--chart-5\", colors.warning],\n [\"--sidebar\", colors.base200],\n [\"--sidebar-foreground\", colors.baseContent],\n [\"--sidebar-primary\", colors.primary],\n [\"--sidebar-primary-foreground\", colors.primaryContent],\n [\"--sidebar-accent\", colors.base300],\n [\"--sidebar-accent-foreground\", colors.baseContent],\n [\"--sidebar-border\", colors.base300],\n [\"--sidebar-ring\", colors.primary],\n [\"--font-sans\", sansFamilyValue],\n [\"--font-mono\", monoFamilyValue],\n ];\n\n return vars.map(([k, v]) => ` ${k}: ${v};`).join(\"\\n\");\n}\n\n/**\n * Generates the full shadcn-compatible CSS output (no DaisyUI, no @plugin blocks).\n * Uses :root for light theme and .dark for dark theme.\n */\nfunction generateShadcnThemeCSS(lightTheme: Theme, darkTheme: Theme | null = null): string {\n const output: string[] = [];\n\n // Tailwind v4 @theme inline — wires CSS vars to Tailwind utility classes\n output.push(`@theme inline {\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n --color-chart-1: var(--chart-1);\n --color-chart-2: var(--chart-2);\n --color-chart-3: var(--chart-3);\n --color-chart-4: var(--chart-4);\n --color-chart-5: var(--chart-5);\n --color-sidebar: var(--sidebar);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-ring: var(--sidebar-ring);\n --font-sans: var(--font-sans);\n --font-mono: var(--font-mono);\n}`);\n\n output.push(\"\");\n\n // Light theme — always :root\n // If only a dark theme is provided (no separate light), use :root, .dark\n const isDarkOnly = !darkTheme && lightTheme.browserColorScheme === \"dark\";\n const lightSelector = isDarkOnly ? \":root, .dark\" : \":root\";\n\n output.push(`/* ${lightTheme.name} theme */`);\n output.push(`${lightSelector} {`);\n output.push(buildShadcnVars(lightTheme));\n output.push(\"}\");\n\n // Dark theme — .dark selector\n if (darkTheme && darkTheme.id !== lightTheme.id) {\n output.push(\"\");\n output.push(`/* ${darkTheme.name} theme (dark) */`);\n output.push(\".dark {\");\n output.push(buildShadcnVars(darkTheme));\n output.push(\"}\");\n }\n\n return output.join(\"\\n\");\n}\n\n/**\n * Unplugin-based theme generator\n */\nexport const upstartTheme = createUnplugin<PluginOptions>((opts) => {\n let generated = false;\n\n return {\n name: \"unplugin-theme-generator\",\n apply: \"serve\",\n\n vite: {\n configResolved() {\n // Generate the CSS file once when config is resolved\n if (generated) return;\n generated = true;\n\n if (!opts?.themes?.light) {\n console.warn(\"No light theme provided to unplugin-theme-generator. A light theme is required.\");\n return;\n }\n\n const outputDir = path.dirname(opts.outputPath);\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const { themes, fontsDisplay = \"swap\", cssFramework = \"daisy-ui\" } = opts;\n\n // Generate fonts.css file (shared by both frameworks)\n const fontsCSS = generateFontsCSS(themes.light, themes.dark, fontsDisplay);\n const fontsCSSPath = path.join(outputDir, \"fonts.css\");\n fs.writeFileSync(fontsCSSPath, fontsCSS);\n\n let cssContent: string;\n\n if (cssFramework === \"shadcn-ui\") {\n cssContent = generateShadcnThemeCSS(themes.light, themes.dark ?? null);\n } else {\n const output: string[] = [];\n\n // Generate @theme block with fonts from both themes\n output.push(generateThemeBlock(themes.light, themes.dark));\n output.push(\"\");\n\n // IMPORTANT; Place dark theme first OTHERWISE daisyUI will not apply it correctly\n // @see https://github.com/saadeghi/daisyui/issues/3921#issuecomment-3059524563\n // Generate @theme block for dark theme if present\n if (themes.dark) {\n output.push(\"/* Dark theme variables */\");\n output.push('@plugin \"daisyui/theme\" {');\n output.push(generateThemeVariables(themes.dark, themes.default));\n output.push(\"}\");\n output.push(\"\");\n }\n\n // Generate @theme block for light theme\n if (themes.light?.id !== themes.dark?.id) {\n output.push(\"/* Light theme variables */\");\n output.push('@plugin \"daisyui/theme\" {');\n output.push(generateThemeVariables(themes.light, themes.default));\n output.push(\"}\");\n output.push(\"\");\n }\n\n cssContent = output.join(\"\\n\");\n }\n\n fs.writeFileSync(opts.outputPath, cssContent);\n },\n },\n };\n});\n\nexport default upstartTheme.vite;\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite-plugin-upstart-theme.js","names":[],"sources":["../src/vite-plugin-upstart-theme.ts"],"sourcesContent":["import { createUnplugin } from \"unplugin\";\nimport {\n GenericFont,\n GoogleFont,\n type Theme,\n fontStacksFonts,\n processTheme,\n} from \"@upstart.gg/sdk/themes/theme\";\nimport fs from \"fs\";\nimport path from \"path\";\n\ninterface PluginOptions {\n themes: {\n light: Theme;\n dark?: Theme;\n default: \"light\" | \"dark\";\n };\n outputPath: string;\n /** Google Fonts display parameter (default: 'swap') */\n fontsDisplay?: \"auto\" | \"block\" | \"swap\" | \"fallback\" | \"optional\";\n /** CSS framework to target (default: 'daisy-ui') */\n cssFramework?: \"daisy-ui\" | \"shadcn-ui\";\n}\n\n/**\n * Collects Google Fonts from a theme's typography configuration\n */\nfunction collectGoogleFonts(theme: Theme) {\n const { typography } = theme;\n if (!typography) return [];\n\n const googleFonts: Array<GoogleFont> = [];\n const seen = new Set<string>();\n\n const addFont = (font?: GenericFont) => {\n if (font?.type === \"google\" && !seen.has(font.family)) {\n seen.add(font.family);\n googleFonts.push(font);\n }\n };\n\n addFont(typography.sans);\n addFont(typography.serif);\n if (typography.others) {\n Object.values(typography.others).forEach(addFont);\n }\n\n return googleFonts;\n}\n\n/**\n * Builds the `family=` parameter of a Google Fonts css2 URL for one font.\n *\n * Weights are requested as a discrete list (`wght@300;400;500`), never as a range\n * (`wght@300..700`). Google drops a family from a multi-family request when a range falls outside\n * its weight axis — single-weight display fonts (Anton, Bebas Neue, Instrument Serif…) and fonts\n * whose axis starts at 400 (Playfair Display) — and the site silently falls back to a system font.\n * A discrete list is tolerated: Google serves the weights the family has and skips the others.\n */\nexport function googleFontFamilyParam(font: Pick<GoogleFont, \"family\" | \"weights\">): string {\n const name = font.family.replace(/ /g, \"+\");\n if (!font.weights?.length) return `family=${name}`;\n const minWeight = Math.min(...font.weights);\n const maxWeight = Math.max(...font.weights);\n if (minWeight === maxWeight) return `family=${name}:wght@${minWeight}`;\n // Every hundred between the bounds: a 300..700 range becomes 300;400;500;600;700.\n const weights: number[] = [];\n for (let w = Math.ceil(minWeight / 100) * 100; w <= maxWeight; w += 100) weights.push(w);\n if (weights[0] !== minWeight) weights.unshift(minWeight);\n if (weights.at(-1) !== maxWeight) weights.push(maxWeight);\n return `family=${name}:wght@${weights.join(\";\")}`;\n}\n\n/**\n * Generates the fonts.css file content with Google Fonts imports\n */\nexport function generateFontsCSS(\n lightTheme: Theme,\n darkTheme: Theme | null = null,\n display: \"auto\" | \"block\" | \"swap\" | \"fallback\" | \"optional\" = \"swap\",\n): string {\n // Collect fonts from both themes\n const allFonts = new Map<string, GoogleFont>();\n\n for (const font of collectGoogleFonts(lightTheme)) {\n allFonts.set(font.family, font);\n }\n\n if (darkTheme) {\n for (const font of collectGoogleFonts(darkTheme)) {\n allFonts.set(font.family, font);\n }\n }\n\n if (allFonts.size === 0) {\n return \"/* No Google Fonts to import */\\n\";\n }\n\n // Build the Google Fonts URL\n const families = Array.from(allFonts.values()).map(googleFontFamilyParam);\n\n const url = `https://fonts.googleapis.com/css2?${families.join(\"&\")}&display=${display}`;\n\n return `/* Google Fonts - Auto-generated by vite-plugin-upstart-theme */\\n@import url(\"${url}\");\\n`;\n}\n\n/**\n * Converts a font definition to a CSS font-family string\n */\nfunction fontToCss(font: Theme[\"typography\"][\"sans\"]): string {\n if (!font) return \"\";\n\n if (font.type === \"stack\") {\n return fontStacksFonts[font.family];\n }\n\n if (font.type === \"google\") {\n // For Google fonts, we just use the family name\n // The actual font loading will be handled separately\n return `\"${font.family}\", system-ui, sans-serif`;\n }\n\n return \"\";\n}\n\n/**\n * Converts camelCase to kebab-case\n */\nfunction camelToKebab(str: string): string {\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\n/**\n * Generates @theme block with font definitions\n */\nfunction generateThemeBlock(\n lightTheme: Theme,\n darkTheme: Theme | null = null,\n defaultTheme: \"light\" | \"dark\" = \"light\",\n): string {\n const fontDefinitions = new Map<string, string>();\n\n // Process light theme fonts\n const lightTypography = lightTheme.typography;\n\n // Add sans font\n if (lightTypography.sans) {\n const fontCss = fontToCss(lightTypography.sans);\n if (fontCss) {\n fontDefinitions.set(\"sans\", fontCss);\n }\n }\n\n // Add serif font\n if (lightTypography.serif) {\n const fontCss = fontToCss(lightTypography.serif);\n if (fontCss) {\n fontDefinitions.set(\"serif\", fontCss);\n }\n }\n\n // Add other fonts\n if (lightTypography.others) {\n for (const [name, font] of Object.entries(lightTypography.others)) {\n const fontCss = fontToCss(font);\n if (fontCss) {\n fontDefinitions.set(name, fontCss);\n }\n }\n }\n\n // Process dark theme fonts if provided\n if (darkTheme) {\n const darkTypography = darkTheme.typography;\n\n if (darkTypography.sans) {\n const fontCss = fontToCss(darkTypography.sans);\n if (fontCss) {\n fontDefinitions.set(\"sans\", fontCss);\n }\n }\n\n if (darkTypography.serif) {\n const fontCss = fontToCss(darkTypography.serif);\n if (fontCss) {\n fontDefinitions.set(\"serif\", fontCss);\n }\n }\n\n if (darkTypography.others) {\n for (const [name, font] of Object.entries(darkTypography.others)) {\n const fontCss = fontToCss(font);\n if (fontCss) {\n fontDefinitions.set(name, fontCss);\n }\n }\n }\n }\n\n const fontLines = Array.from(fontDefinitions.entries())\n .map(([name, value]) => ` --font-${name}: ${value};`)\n .join(\"\\n\");\n\n return `/* Fonts in the tailwind @theme block */\n@theme {\n ${fontLines}\n}\n\n/* Exclude properties since it produce warnings and is not important */\n@plugin \"daisyui\" {\n exclude: properties;\n themes: ${lightTheme.id} ${defaultTheme === \"light\" || !darkTheme ? \"--default\" : \"\"}${darkTheme?.id && darkTheme?.id !== lightTheme.id ? `, ${darkTheme.id} ${defaultTheme === \"dark\" ? \"--default\" : \"--prefersdark\"}` : \"\"};\n logs: true;\n}\n`;\n}\n\n/**\n * Generates @theme block for a single theme with colors and layout variables\n */\nfunction generateThemeVariables(theme: Theme, defaultTheme: \"light\" | \"dark\"): string {\n const processed = processTheme(theme);\n const lines: string[] = [\n ` name: \"${processed.id}\";`,\n ` color-scheme: ${processed.browserColorScheme};`,\n ...((defaultTheme === \"dark\" && theme.browserColorScheme === \"dark\") ||\n (defaultTheme === \"light\" && theme.browserColorScheme === \"light\")\n ? [` default: true;`]\n : []),\n // ...(type === \"dark\" && defaultTheme !== \"dark\" ? [` prefersdark: true;`] : [` prefersdark: false;`]),\n ];\n\n // Color variables\n for (const [colorKey, colorValue] of Object.entries(processed.colors)) {\n const kebabKey = [\"base100\", \"base200\", \"base300\"].includes(colorKey)\n ? colorKey.replace(\"base\", \"base-\")\n : camelToKebab(colorKey);\n lines.push(` --color-${kebabKey}: ${colorValue};`);\n }\n\n lines.push(\"\");\n\n // Layout variables\n lines.push(` --radius-selector: ${processed.radiusSelector};`);\n lines.push(` --radius-field: ${processed.radiusField};`);\n lines.push(` --radius-box: ${processed.radiusBox};`);\n lines.push(` --size-selector: ${processed.sizeSelector};`);\n lines.push(` --size-field: ${processed.sizeField};`);\n lines.push(` --border: ${processed.border};`);\n lines.push(` --depth: ${processed.depth ? 1 : 0};`);\n lines.push(` --noise: 0;`);\n\n // return the generated lines\n return lines.join(\"\\n\");\n}\n\n/**\n * Builds the CSS variable block for a single shadcn theme (`:root` or `.dark`)\n */\nfunction buildShadcnVars(theme: Theme): string {\n const processed = processTheme(theme);\n const { colors } = processed;\n\n const monoFont = theme.typography?.others?.mono;\n const monoFamilyValue = monoFont ? fontToCss(monoFont) : \"ui-monospace, monospace\";\n const sansFamilyValue = theme.typography?.sans\n ? fontToCss(theme.typography.sans)\n : \"ui-sans-serif, system-ui, sans-serif\";\n\n const vars: [string, string][] = [\n [\"--radius\", processed.radiusField],\n [\"--background\", colors.base100],\n [\"--foreground\", colors.baseContent],\n [\"--card\", colors.base200],\n [\"--card-foreground\", colors.baseContent],\n [\"--popover\", colors.base200],\n [\"--popover-foreground\", colors.baseContent],\n [\"--primary\", colors.primary],\n [\"--primary-foreground\", colors.primaryContent],\n [\"--secondary\", colors.secondary],\n [\"--secondary-foreground\", colors.secondaryContent],\n [\"--muted\", colors.neutral],\n [\"--muted-foreground\", colors.neutralContent],\n // shadcn --accent = hover surface, NOT the vibrant accent color\n [\"--accent\", colors.base300],\n [\"--accent-foreground\", colors.baseContent],\n [\"--destructive\", colors.error],\n [\"--border\", colors.base300],\n [\"--input\", colors.base300],\n [\"--ring\", colors.primary],\n [\"--chart-1\", colors.primary],\n [\"--chart-2\", colors.secondary],\n // vibrant accent goes in chart-3\n [\"--chart-3\", colors.accent],\n [\"--chart-4\", colors.success],\n [\"--chart-5\", colors.warning],\n [\"--sidebar\", colors.base200],\n [\"--sidebar-foreground\", colors.baseContent],\n [\"--sidebar-primary\", colors.primary],\n [\"--sidebar-primary-foreground\", colors.primaryContent],\n [\"--sidebar-accent\", colors.base300],\n [\"--sidebar-accent-foreground\", colors.baseContent],\n [\"--sidebar-border\", colors.base300],\n [\"--sidebar-ring\", colors.primary],\n [\"--font-sans\", sansFamilyValue],\n [\"--font-mono\", monoFamilyValue],\n ];\n\n return vars.map(([k, v]) => ` ${k}: ${v};`).join(\"\\n\");\n}\n\n/**\n * Generates the full shadcn-compatible CSS output (no DaisyUI, no @plugin blocks).\n * Uses :root for light theme and .dark for dark theme.\n */\nfunction generateShadcnThemeCSS(lightTheme: Theme, darkTheme: Theme | null = null): string {\n const output: string[] = [];\n\n // Tailwind v4 @theme inline — wires CSS vars to Tailwind utility classes\n output.push(`@theme inline {\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n --color-chart-1: var(--chart-1);\n --color-chart-2: var(--chart-2);\n --color-chart-3: var(--chart-3);\n --color-chart-4: var(--chart-4);\n --color-chart-5: var(--chart-5);\n --color-sidebar: var(--sidebar);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-ring: var(--sidebar-ring);\n --font-sans: var(--font-sans);\n --font-mono: var(--font-mono);\n}`);\n\n output.push(\"\");\n\n // Light theme — always :root\n // If only a dark theme is provided (no separate light), use :root, .dark\n const isDarkOnly = !darkTheme && lightTheme.browserColorScheme === \"dark\";\n const lightSelector = isDarkOnly ? \":root, .dark\" : \":root\";\n\n output.push(`/* ${lightTheme.name} theme */`);\n output.push(`${lightSelector} {`);\n output.push(buildShadcnVars(lightTheme));\n output.push(\"}\");\n\n // Dark theme — .dark selector\n if (darkTheme && darkTheme.id !== lightTheme.id) {\n output.push(\"\");\n output.push(`/* ${darkTheme.name} theme (dark) */`);\n output.push(\".dark {\");\n output.push(buildShadcnVars(darkTheme));\n output.push(\"}\");\n }\n\n return output.join(\"\\n\");\n}\n\n/**\n * Unplugin-based theme generator\n */\nexport const upstartTheme = createUnplugin<PluginOptions>((opts) => {\n let generated = false;\n\n return {\n name: \"unplugin-theme-generator\",\n apply: \"serve\",\n\n vite: {\n configResolved() {\n // Generate the CSS file once when config is resolved\n if (generated) return;\n generated = true;\n\n if (!opts?.themes?.light) {\n console.warn(\"No light theme provided to unplugin-theme-generator. A light theme is required.\");\n return;\n }\n\n const outputDir = path.dirname(opts.outputPath);\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const { themes, fontsDisplay = \"swap\", cssFramework = \"daisy-ui\" } = opts;\n\n // Generate fonts.css file (shared by both frameworks)\n const fontsCSS = generateFontsCSS(themes.light, themes.dark, fontsDisplay);\n const fontsCSSPath = path.join(outputDir, \"fonts.css\");\n fs.writeFileSync(fontsCSSPath, fontsCSS);\n\n let cssContent: string;\n\n if (cssFramework === \"shadcn-ui\") {\n cssContent = generateShadcnThemeCSS(themes.light, themes.dark ?? null);\n } else {\n const output: string[] = [];\n\n // Generate @theme block with fonts from both themes\n output.push(generateThemeBlock(themes.light, themes.dark));\n output.push(\"\");\n\n // IMPORTANT; Place dark theme first OTHERWISE daisyUI will not apply it correctly\n // @see https://github.com/saadeghi/daisyui/issues/3921#issuecomment-3059524563\n // Generate @theme block for dark theme if present\n if (themes.dark) {\n output.push(\"/* Dark theme variables */\");\n output.push('@plugin \"daisyui/theme\" {');\n output.push(generateThemeVariables(themes.dark, themes.default));\n output.push(\"}\");\n output.push(\"\");\n }\n\n // Generate @theme block for light theme\n if (themes.light?.id !== themes.dark?.id) {\n output.push(\"/* Light theme variables */\");\n output.push('@plugin \"daisyui/theme\" {');\n output.push(generateThemeVariables(themes.light, themes.default));\n output.push(\"}\");\n output.push(\"\");\n }\n\n cssContent = output.join(\"\\n\");\n }\n\n fs.writeFileSync(opts.outputPath, cssContent);\n },\n },\n };\n});\n\nexport default upstartTheme.vite;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstart.gg/vite-plugins",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -23,7 +23,7 @@
23
23
  "oxc-parser": "0.101.0",
24
24
  "unplugin": "^2.3.11",
25
25
  "zimmerframe": "^1.1.4",
26
- "@upstart.gg/sdk": "^0.1.78"
26
+ "@upstart.gg/sdk": "^0.1.80"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@rolldown/binding-linux-arm64-gnu": "1.0.0",
@@ -118,7 +118,7 @@
118
118
  },
119
119
  "peerDependencies": {
120
120
  "zod": "4.3.6",
121
- "@upstart.gg/sdk": "^0.1.78"
121
+ "@upstart.gg/sdk": "^0.1.80"
122
122
  },
123
123
  "author": "Upstart",
124
124
  "publishConfig": {
@@ -0,0 +1,55 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { defaultTheme, type Theme } from "@upstart.gg/sdk/themes/theme";
3
+ import { generateFontsCSS, googleFontFamilyParam } from "../vite-plugin-upstart-theme";
4
+
5
+ describe("googleFontFamilyParam", () => {
6
+ test("expands a weight range into a discrete list", () => {
7
+ expect(googleFontFamilyParam({ family: "Playfair Display", weights: [300, 700] })).toBe(
8
+ "family=Playfair+Display:wght@300;400;500;600;700",
9
+ );
10
+ });
11
+
12
+ test("never emits a range, which Google rejects for fonts outside the axis", () => {
13
+ for (const weights of [
14
+ [300, 700],
15
+ [100, 900],
16
+ [400, 800],
17
+ ]) {
18
+ expect(googleFontFamilyParam({ family: "Anton", weights })).not.toContain("..");
19
+ }
20
+ });
21
+
22
+ test("keeps bounds that are not multiples of 100", () => {
23
+ expect(googleFontFamilyParam({ family: "Inter", weights: [350, 650] })).toBe(
24
+ "family=Inter:wght@350;400;500;600;650",
25
+ );
26
+ });
27
+
28
+ test("requests a single weight when both bounds are equal", () => {
29
+ expect(googleFontFamilyParam({ family: "Bebas Neue", weights: [400, 400] })).toBe(
30
+ "family=Bebas+Neue:wght@400",
31
+ );
32
+ });
33
+
34
+ test("omits the axis when no weights are given", () => {
35
+ expect(googleFontFamilyParam({ family: "Instrument Serif" })).toBe("family=Instrument+Serif");
36
+ });
37
+ });
38
+
39
+ describe("generateFontsCSS", () => {
40
+ test("imports every Google font of both themes once, with discrete weights", () => {
41
+ const dark: Theme = {
42
+ ...defaultTheme,
43
+ typography: {
44
+ ...defaultTheme.typography,
45
+ others: { display: { type: "google", family: "Anton", weights: [400, 400] } },
46
+ },
47
+ };
48
+ const css = generateFontsCSS(defaultTheme, dark);
49
+ expect(css).toContain("family=Inter:wght@300;400;500;600;700");
50
+ expect(css).toContain("family=Playfair+Display:wght@300;400;500;600;700");
51
+ expect(css).toContain("family=Anton:wght@400");
52
+ expect(css).not.toContain("..");
53
+ expect(css.match(/family=Inter/g)).toHaveLength(1);
54
+ });
55
+ });
@@ -48,10 +48,33 @@ function collectGoogleFonts(theme: Theme) {
48
48
  return googleFonts;
49
49
  }
50
50
 
51
+ /**
52
+ * Builds the `family=` parameter of a Google Fonts css2 URL for one font.
53
+ *
54
+ * Weights are requested as a discrete list (`wght@300;400;500`), never as a range
55
+ * (`wght@300..700`). Google drops a family from a multi-family request when a range falls outside
56
+ * its weight axis — single-weight display fonts (Anton, Bebas Neue, Instrument Serif…) and fonts
57
+ * whose axis starts at 400 (Playfair Display) — and the site silently falls back to a system font.
58
+ * A discrete list is tolerated: Google serves the weights the family has and skips the others.
59
+ */
60
+ export function googleFontFamilyParam(font: Pick<GoogleFont, "family" | "weights">): string {
61
+ const name = font.family.replace(/ /g, "+");
62
+ if (!font.weights?.length) return `family=${name}`;
63
+ const minWeight = Math.min(...font.weights);
64
+ const maxWeight = Math.max(...font.weights);
65
+ if (minWeight === maxWeight) return `family=${name}:wght@${minWeight}`;
66
+ // Every hundred between the bounds: a 300..700 range becomes 300;400;500;600;700.
67
+ const weights: number[] = [];
68
+ for (let w = Math.ceil(minWeight / 100) * 100; w <= maxWeight; w += 100) weights.push(w);
69
+ if (weights[0] !== minWeight) weights.unshift(minWeight);
70
+ if (weights.at(-1) !== maxWeight) weights.push(maxWeight);
71
+ return `family=${name}:wght@${weights.join(";")}`;
72
+ }
73
+
51
74
  /**
52
75
  * Generates the fonts.css file content with Google Fonts imports
53
76
  */
54
- function generateFontsCSS(
77
+ export function generateFontsCSS(
55
78
  lightTheme: Theme,
56
79
  darkTheme: Theme | null = null,
57
80
  display: "auto" | "block" | "swap" | "fallback" | "optional" = "swap",
@@ -74,16 +97,7 @@ function generateFontsCSS(
74
97
  }
75
98
 
76
99
  // Build the Google Fonts URL
77
- const families = Array.from(allFonts.values()).map((font) => {
78
- const name = font.family.replace(/ /g, "+");
79
- if (font.weights && font.weights.length > 1 && font.weights[0] !== font.weights.at(-1)) {
80
- // Take min weight and max weight
81
- const minWeight = Math.min(...font.weights);
82
- const maxWeight = Math.max(...font.weights);
83
- return `family=${name}:wght@${minWeight}..${maxWeight}`;
84
- }
85
- return `family=${name}`;
86
- });
100
+ const families = Array.from(allFonts.values()).map(googleFontFamilyParam);
87
101
 
88
102
  const url = `https://fonts.googleapis.com/css2?${families.join("&")}&display=${display}`;
89
103