Add Hamkar UI reset-to-default snapshot and restore button.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 20:51:33 +03:30
co-authored by Cursor
parent 8630040b90
commit d5f0bf84ef
2 changed files with 236 additions and 52 deletions
+109 -39
View File
@@ -2,6 +2,28 @@ export type HamkarPricingBoxTheme = '1' | '2' | '3' | '4'
export type HamkarOrderUi = 'new' | 'old'
export type HamkarPriceFontSize = 'کوچک' | 'متوسط' | 'بزرگ' | 'بزرگ‌تر'
export type HamkarBrandColors = {
/** Registry `color` → CSS `--bg2` (only when mode=dynamicMode) */
primaryColor: string
/** Registry `secondary_color` → CSS `--colorPrimary` (dynamicMode) */
secondaryColor: string
/** Registry `background_color` → CSS `--bg` (dynamicMode) */
backgroundColor: string
}
export type HamkarUiSettings = {
pricing_box_theme: HamkarPricingBoxTheme
price_font_size: HamkarPriceFontSize
orderUI: HamkarOrderUi
}
/** Immutable factory look — set once, never overwritten on normal save */
export type HamkarAppearanceSnapshot = {
registryMode: string
brand: HamkarBrandColors
settings: HamkarUiSettings
}
export type HamkarUiConfig = {
kind: 'hamkar'
/**
@@ -11,19 +33,10 @@ export type HamkarUiConfig = {
settingsManaged: boolean
/** Named CSS mode from live registry (Montpar, limangold, …) or dynamicMode */
registryMode: string
brand: {
/** Registry `color` → CSS `--bg2` (only when mode=dynamicMode) */
primaryColor: string
/** Registry `secondary_color` → CSS `--colorPrimary` (dynamicMode) */
secondaryColor: string
/** Registry `background_color` → CSS `--bg` (dynamicMode) */
backgroundColor: string
}
settings: {
pricing_box_theme: HamkarPricingBoxTheme
price_font_size: HamkarPriceFontSize
orderUI: HamkarOrderUi
}
brand: HamkarBrandColors
settings: HamkarUiSettings
/** Original appearance before SuperAdmin color edits (reset target) */
defaultSnapshot?: HamkarAppearanceSnapshot
}
export const HAMKAR_PRICING_BOX_THEME_OPTIONS: {
@@ -58,6 +71,34 @@ function asColor(value: unknown): string {
return s
}
function normalizeSettings(raw: unknown): HamkarUiSettings {
const base = createDefaultHamkarUiConfig().settings
const settings = raw && typeof raw === 'object' ? (raw as Record<string, any>) : {}
const theme = String(settings.pricing_box_theme ?? base.pricing_box_theme)
const orderUI = String(settings.orderUI ?? base.orderUI)
const font = String(settings.price_font_size ?? base.price_font_size)
return {
pricing_box_theme: (['1', '2', '3', '4'].includes(theme)
? theme
: base.pricing_box_theme) as HamkarPricingBoxTheme,
orderUI: (orderUI === 'old' || orderUI === 'new'
? orderUI
: base.orderUI) as HamkarOrderUi,
price_font_size: (HAMKAR_PRICE_FONT_SIZE_OPTIONS.some((o) => o.value === font)
? font
: base.price_font_size) as HamkarPriceFontSize,
}
}
function normalizeBrand(raw: unknown): HamkarBrandColors {
const brand = raw && typeof raw === 'object' ? (raw as Record<string, any>) : {}
return {
primaryColor: asColor(brand.primaryColor),
secondaryColor: asColor(brand.secondaryColor),
backgroundColor: asColor(brand.backgroundColor),
}
}
export function createDefaultHamkarUiConfig(): HamkarUiConfig {
return {
kind: 'hamkar',
@@ -76,55 +117,74 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig {
}
}
export function cloneHamkarSnapshot(config: {
registryMode: string
brand: HamkarBrandColors
settings: HamkarUiSettings
}): HamkarAppearanceSnapshot {
return {
registryMode: String(config.registryMode || 'dynamicMode').trim() || 'dynamicMode',
brand: { ...config.brand },
settings: { ...config.settings },
}
}
export function normalizeHamkarUiConfig(raw: unknown): HamkarUiConfig {
const base = createDefaultHamkarUiConfig()
if (!raw || typeof raw !== 'object') return base
const input = raw as Record<string, any>
const brand = input.brand && typeof input.brand === 'object' ? input.brand : {}
const settings =
input.settings && typeof input.settings === 'object' ? input.settings : {}
const theme = String(settings.pricing_box_theme ?? base.settings.pricing_box_theme)
const orderUI = String(settings.orderUI ?? base.settings.orderUI)
const font = String(settings.price_font_size ?? base.settings.price_font_size)
const registryMode = String(input.registryMode || base.registryMode).trim() || 'dynamicMode'
const settings = normalizeSettings(input.settings)
const brand = normalizeBrand(input.brand)
let defaultSnapshot: HamkarAppearanceSnapshot | undefined
if (input.defaultSnapshot && typeof input.defaultSnapshot === 'object') {
const snap = input.defaultSnapshot as Record<string, any>
defaultSnapshot = {
registryMode: String(snap.registryMode || registryMode).trim() || 'dynamicMode',
brand: normalizeBrand(snap.brand),
settings: normalizeSettings(snap.settings ?? settings),
}
}
return {
kind: 'hamkar',
settingsManaged: Boolean(input.settingsManaged),
registryMode,
brand: {
// Do not invent colors — empty means null in registry/get_user
primaryColor: asColor(brand.primaryColor),
secondaryColor: asColor(brand.secondaryColor),
backgroundColor: asColor(brand.backgroundColor),
},
settings: {
pricing_box_theme: (['1', '2', '3', '4'].includes(theme)
? theme
: base.settings.pricing_box_theme) as HamkarPricingBoxTheme,
orderUI: (orderUI === 'old' || orderUI === 'new'
? orderUI
: base.settings.orderUI) as HamkarOrderUi,
price_font_size: (HAMKAR_PRICE_FONT_SIZE_OPTIONS.some((o) => o.value === font)
? font
: base.settings.price_font_size) as HamkarPriceFontSize,
},
brand,
settings,
defaultSnapshot,
}
}
/** Payload for API: empty color strings → null */
/** Payload for API: empty color strings → null; always keep defaultSnapshot if present */
export function hamkarUiToApiPayload(config: HamkarUiConfig) {
const normalized = normalizeHamkarUiConfig(config)
const emptyToNull = (v: string) => (v.trim() ? v.trim() : null)
const snap = normalized.defaultSnapshot
? {
registryMode: normalized.defaultSnapshot.registryMode,
brand: {
primaryColor: emptyToNull(normalized.defaultSnapshot.brand.primaryColor),
secondaryColor: emptyToNull(normalized.defaultSnapshot.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.defaultSnapshot.brand.backgroundColor),
},
settings: { ...normalized.defaultSnapshot.settings },
}
: undefined
return {
...normalized,
kind: 'hamkar' as const,
settingsManaged: normalized.settingsManaged,
registryMode: normalized.registryMode,
brand: {
primaryColor: emptyToNull(normalized.brand.primaryColor),
secondaryColor: emptyToNull(normalized.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.brand.backgroundColor),
},
settings: { ...normalized.settings },
...(snap ? { defaultSnapshot: snap } : {}),
}
}
@@ -137,3 +197,13 @@ export function hamkarUiToSettingsList(config: HamkarUiConfig) {
{ key: 'orderUI', value: config.settings.orderUI },
]
}
/** Resolve original mode from tenant notes (`registryMode:X` or `mode:X`). */
export function registryModeFromNotes(notes?: string | null): string | null {
if (!notes) return null
return (
notes.match(/registryMode:([^\s|]+)/)?.[1] ||
notes.match(/(?:^|\|)\s*mode:([^\s|]+)/)?.[1] ||
null
)
}