274 lines
6.7 KiB
TypeScript
274 lines
6.7 KiB
TypeScript
// ── Preset encoding/decoding (matches shadcn v2 format) ─────────────
|
|
|
|
const PRESET_STYLES = ["nova", "vega", "maia", "lyra", "mira"] as const
|
|
|
|
const PRESET_BASE_COLORS = [
|
|
"neutral",
|
|
"stone",
|
|
"zinc",
|
|
"gray",
|
|
"mauve",
|
|
"olive",
|
|
"mist",
|
|
"taupe",
|
|
] as const
|
|
|
|
const PRESET_THEMES = [
|
|
"neutral",
|
|
"stone",
|
|
"zinc",
|
|
"gray",
|
|
"amber",
|
|
"blue",
|
|
"cyan",
|
|
"emerald",
|
|
"fuchsia",
|
|
"green",
|
|
"indigo",
|
|
"lime",
|
|
"orange",
|
|
"pink",
|
|
"purple",
|
|
"red",
|
|
"rose",
|
|
"sky",
|
|
"teal",
|
|
"violet",
|
|
"yellow",
|
|
"mauve",
|
|
"olive",
|
|
"mist",
|
|
"taupe",
|
|
] as const
|
|
|
|
const PRESET_ICON_LIBRARIES = [
|
|
"lucide",
|
|
"hugeicons",
|
|
"tabler",
|
|
"phosphor",
|
|
"remixicon",
|
|
] as const
|
|
|
|
const PRESET_FONTS = [
|
|
"inter",
|
|
"noto-sans",
|
|
"nunito-sans",
|
|
"figtree",
|
|
"roboto",
|
|
"raleway",
|
|
"dm-sans",
|
|
"public-sans",
|
|
"outfit",
|
|
"jetbrains-mono",
|
|
"geist",
|
|
"geist-mono",
|
|
"lora",
|
|
"merriweather",
|
|
"playfair-display",
|
|
"noto-serif",
|
|
"roboto-slab",
|
|
"oxanium",
|
|
"manrope",
|
|
"space-grotesk",
|
|
"montserrat",
|
|
"ibm-plex-sans",
|
|
"source-sans-3",
|
|
"instrument-sans",
|
|
] as const
|
|
|
|
const PRESET_FONT_HEADINGS = ["inherit", ...PRESET_FONTS] as const
|
|
|
|
const PRESET_RADII = ["default", "none", "small", "medium", "large"] as const
|
|
|
|
const PRESET_MENU_ACCENTS = ["subtle", "bold"] as const
|
|
|
|
const PRESET_MENU_COLORS = [
|
|
"default",
|
|
"inverted",
|
|
"default-translucent",
|
|
"inverted-translucent",
|
|
] as const
|
|
|
|
/** V2 field layout for bit-packing (order must match shadcn exactly) */
|
|
const PRESET_FIELDS_V2 = [
|
|
{ key: "menuColor", values: PRESET_MENU_COLORS, bits: 3 },
|
|
{ key: "menuAccent", values: PRESET_MENU_ACCENTS, bits: 3 },
|
|
{ key: "radius", values: PRESET_RADII, bits: 4 },
|
|
{ key: "font", values: PRESET_FONTS, bits: 6 },
|
|
{ key: "iconLibrary", values: PRESET_ICON_LIBRARIES, bits: 6 },
|
|
{ key: "theme", values: PRESET_THEMES, bits: 6 },
|
|
{ key: "baseColor", values: PRESET_BASE_COLORS, bits: 6 },
|
|
{ key: "style", values: PRESET_STYLES, bits: 6 },
|
|
{ key: "chartColor", values: PRESET_THEMES, bits: 6 },
|
|
{ key: "fontHeading", values: PRESET_FONT_HEADINGS, bits: 5 },
|
|
] as const
|
|
|
|
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
|
|
function toBase62(num: number): string {
|
|
if (num === 0) return "0"
|
|
let result = ""
|
|
let n = num
|
|
while (n > 0) {
|
|
result = BASE62[n % 62] + result
|
|
n = Math.floor(n / 62)
|
|
}
|
|
return result
|
|
}
|
|
|
|
/** Encode a preset config into a compact base62 code (v2 format). */
|
|
export function encodePreset(config: PresetCodeConfig): string {
|
|
const defaults: Record<string, string> = {
|
|
menuColor: "default",
|
|
menuAccent: "subtle",
|
|
radius: "default",
|
|
font: "inter",
|
|
iconLibrary: "lucide",
|
|
theme: "neutral",
|
|
baseColor: "neutral",
|
|
style: "nova",
|
|
chartColor: config.theme ?? "neutral",
|
|
fontHeading: "inherit",
|
|
}
|
|
const merged: Record<string, string> = { ...defaults }
|
|
for (const [k, v] of Object.entries(config)) {
|
|
if (v) merged[k] = v
|
|
}
|
|
let bits = 0
|
|
let offset = 0
|
|
for (const field of PRESET_FIELDS_V2) {
|
|
const idx = (field.values as readonly string[]).indexOf(
|
|
merged[field.key] ?? ""
|
|
)
|
|
bits += (idx === -1 ? 0 : idx) * 2 ** offset
|
|
offset += field.bits
|
|
}
|
|
return "b" + toBase62(bits)
|
|
}
|
|
|
|
// ── Config types ────────────────────────────────────────────────────
|
|
|
|
/** Fields that are encoded into the preset code (sent to CLI & preview). */
|
|
export interface PresetCodeConfig {
|
|
style: string
|
|
baseColor: string
|
|
theme: string
|
|
chartColor: string
|
|
iconLibrary: string
|
|
font: string
|
|
fontHeading: string
|
|
radius: string
|
|
menuAccent: string
|
|
menuColor: string
|
|
}
|
|
|
|
/** Full UI config (preset fields + non-preset fields like base/template). */
|
|
export interface ShadcnPresetConfig extends PresetCodeConfig {
|
|
base: string
|
|
template: string
|
|
}
|
|
|
|
export const DEFAULT_PRESET_CONFIG: ShadcnPresetConfig = {
|
|
base: "radix",
|
|
style: "nova",
|
|
baseColor: "neutral",
|
|
theme: "orange",
|
|
chartColor: "orange",
|
|
iconLibrary: "lucide",
|
|
font: "inter",
|
|
fontHeading: "inherit",
|
|
radius: "default",
|
|
menuAccent: "subtle",
|
|
menuColor: "default",
|
|
template: "start",
|
|
}
|
|
|
|
// ── UI option arrays ────────────────────────────────────────────────
|
|
|
|
export const BASE_OPTIONS = [
|
|
{ value: "radix", label: "Radix" },
|
|
{ value: "base", label: "Base" },
|
|
]
|
|
|
|
export const STYLE_OPTIONS = PRESET_STYLES.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const BASE_COLOR_OPTIONS = PRESET_BASE_COLORS.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const THEME_OPTIONS = PRESET_THEMES.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const ICON_LIBRARY_OPTIONS = PRESET_ICON_LIBRARIES.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const FONT_OPTIONS = PRESET_FONTS.map((v) => ({
|
|
value: v,
|
|
label: v
|
|
.split("-")
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(" "),
|
|
}))
|
|
|
|
export const FONT_HEADING_OPTIONS = PRESET_FONT_HEADINGS.map((v) => ({
|
|
value: v,
|
|
label:
|
|
v === "inherit"
|
|
? "Inherit"
|
|
: v
|
|
.split("-")
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(" "),
|
|
}))
|
|
|
|
export const MENU_ACCENT_OPTIONS = PRESET_MENU_ACCENTS.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const MENU_COLOR_OPTIONS = PRESET_MENU_COLORS.map((v) => ({
|
|
value: v,
|
|
label: v
|
|
.split("-")
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(" "),
|
|
}))
|
|
|
|
export const RADIUS_OPTIONS = PRESET_RADII.map((v) => ({
|
|
value: v,
|
|
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
}))
|
|
|
|
export const TEMPLATE_OPTIONS = [{ value: "start", label: "Start" }]
|
|
|
|
export const FRAMEWORK_OPTIONS = [
|
|
{ value: "next", label: "Next.js" },
|
|
{ value: "vite", label: "Vite" },
|
|
{ value: "start", label: "TanStack Start" },
|
|
{ value: "react-router", label: "React Router" },
|
|
{ value: "laravel", label: "Laravel" },
|
|
{ value: "astro", label: "Astro" },
|
|
]
|
|
|
|
export const PACKAGE_MANAGER_OPTIONS = [
|
|
{ value: "pnpm", label: "pnpm" },
|
|
{ value: "npm", label: "npm" },
|
|
{ value: "yarn", label: "yarn" },
|
|
{ value: "bun", label: "bun" },
|
|
]
|
|
|
|
// ── URL builders ────────────────────────────────────────────────────
|
|
|
|
/** Build the preview iframe URL using a preset code. */
|
|
export function buildPreviewUrl(_base: string, presetCode: string): string {
|
|
return `https://ui.shadcn.com/preview/radix/preview-02?preset=${presetCode}`
|
|
}
|