feat(hamkar-ui): add menu icon and active icon color pickers

Extend Hamkar appearance with separate default/selected icon colors and show them in the mobile preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 18:06:27 +03:30
co-authored by Cursor
parent 71e6655f90
commit 30dbb1892d
2 changed files with 83 additions and 3 deletions
+14
View File
@@ -9,6 +9,10 @@ export type HamkarBrandColors = {
secondaryColor: string
/** Registry `background_color` → CSS `--bg` (dynamicMode) */
backgroundColor: string
/** Menu / header icon stroke (brand_chrome) */
menuIconColor: string
/** Selected menu icon + label (brand_chrome) */
menuIconActiveColor: string
}
export type HamkarUiSettings = {
@@ -102,6 +106,8 @@ function normalizeBrand(raw: unknown): HamkarBrandColors {
primaryColor: asColor(brand.primaryColor),
secondaryColor: asColor(brand.secondaryColor),
backgroundColor: asColor(brand.backgroundColor),
menuIconColor: asColor(brand.menuIconColor),
menuIconActiveColor: asColor(brand.menuIconActiveColor),
}
}
@@ -115,6 +121,8 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig {
primaryColor: '',
secondaryColor: '',
backgroundColor: '',
menuIconColor: '',
menuIconActiveColor: '',
},
settings: {
pricing_box_theme: '1',
@@ -177,6 +185,10 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
primaryColor: emptyToNull(normalized.defaultSnapshot.brand.primaryColor),
secondaryColor: emptyToNull(normalized.defaultSnapshot.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.defaultSnapshot.brand.backgroundColor),
menuIconColor: emptyToNull(normalized.defaultSnapshot.brand.menuIconColor),
menuIconActiveColor: emptyToNull(
normalized.defaultSnapshot.brand.menuIconActiveColor,
),
},
settings: { ...normalized.defaultSnapshot.settings },
}
@@ -191,6 +203,8 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
primaryColor: emptyToNull(normalized.brand.primaryColor),
secondaryColor: emptyToNull(normalized.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.brand.backgroundColor),
menuIconColor: emptyToNull(normalized.brand.menuIconColor),
menuIconActiveColor: emptyToNull(normalized.brand.menuIconActiveColor),
},
settings: { ...normalized.settings },
...(snap ? { defaultSnapshot: snap } : {}),
+69 -3
View File
@@ -75,6 +75,8 @@ const palette = computed(() => ({
header: swatchHex(config.brand.primaryColor, '#ffffff'),
accent: swatchHex(config.brand.secondaryColor, '#2563EB'),
page: swatchHex(config.brand.backgroundColor, '#fafafa'),
menuIcon: swatchHex(config.brand.menuIconColor, textOn(swatchHex(config.brand.primaryColor, '#ffffff'))),
menuIconActive: swatchHex(config.brand.menuIconActiveColor, '#2563EB'),
}))
const previewChrome = computed(() => {
@@ -99,8 +101,14 @@ const previewChrome = computed(() => {
color: onHeader,
borderTop: headerEdge,
},
brandFooter: {
background: p.header,
color: p.menuIcon,
},
accent: p.accent,
muted: onHeader === '#ffffff' ? 'rgba(255,255,255,0.72)' : '#616161',
menuIcon: p.menuIcon,
menuIconActive: p.menuIconActive,
muted: p.menuIcon,
}
})
@@ -120,6 +128,14 @@ function seedEditableColors(normalized: HamkarUiConfig) {
if (!normalized.brand.secondaryColor) normalized.brand.secondaryColor = effective.accent
if (!normalized.brand.backgroundColor) normalized.brand.backgroundColor = effective.pageBg
}
if (!normalized.brand.menuIconColor) {
normalized.brand.menuIconColor = textOn(
swatchHex(normalized.brand.primaryColor, effective.headerBg),
)
}
if (!normalized.brand.menuIconActiveColor) {
normalized.brand.menuIconActiveColor = effective.accent
}
}
function ensureDefaultSnapshot(
@@ -148,6 +164,8 @@ function ensureDefaultSnapshot(
primaryColor: String(opts.primaryColor || normalized.brand.primaryColor || ''),
secondaryColor: String(opts.secondaryColor || normalized.brand.secondaryColor || ''),
backgroundColor: String(opts.backgroundColor || normalized.brand.backgroundColor || ''),
menuIconColor: String(normalized.brand.menuIconColor || ''),
menuIconActiveColor: String(normalized.brand.menuIconActiveColor || ''),
},
settings: { ...normalized.settings },
})
@@ -267,6 +285,8 @@ function resetToDefault() {
config.brand.primaryColor = snap.brand.primaryColor
config.brand.secondaryColor = snap.brand.secondaryColor
config.brand.backgroundColor = snap.brand.backgroundColor
config.brand.menuIconColor = snap.brand.menuIconColor
config.brand.menuIconActiveColor = snap.brand.menuIconActiveColor
config.settings = { ...snap.settings }
// Named factory modes must not keep brand chrome overrides.
config.brandManaged = snap.registryMode === 'dynamicMode'
@@ -328,15 +348,20 @@ onMounted(load)
v-for="(item, i) in NAV_ITEMS"
:key="item"
class="app-preview-nav"
:style="{ color: i === 0 ? previewChrome.accent : previewChrome.muted }"
:style="{ color: i === 0 ? previewChrome.menuIconActive : previewChrome.menuIcon }"
>
<span
class="app-preview-dot"
:style="{ background: i === 0 ? previewChrome.accent : previewChrome.muted }"
:style="{
background: i === 0 ? previewChrome.menuIconActive : previewChrome.menuIcon,
}"
/>
{{ item }}
</div>
</div>
<div class="app-preview-brand" :style="previewChrome.brandFooter">
توسعه و طراحی توسط
</div>
</div>
</section>
@@ -399,6 +424,40 @@ onMounted(load)
class="palette-hex"
/>
</label>
<label class="palette-card">
<span class="palette-title">آیکون منو</span>
<span class="palette-swatch" :style="{ background: palette.menuIcon }">
<input
:value="palette.menuIcon"
type="color"
@input="config.brand.menuIconColor = ($event.target as HTMLInputElement).value"
/>
</span>
<a-input
v-model:value="config.brand.menuIconColor"
dir="ltr"
size="large"
class="palette-hex"
/>
</label>
<label class="palette-card">
<span class="palette-title">آیکون فعال</span>
<span class="palette-swatch" :style="{ background: palette.menuIconActive }">
<input
:value="palette.menuIconActive"
type="color"
@input="config.brand.menuIconActiveColor = ($event.target as HTMLInputElement).value"
/>
</span>
<a-input
v-model:value="config.brand.menuIconActiveColor"
dir="ltr"
size="large"
class="palette-hex"
/>
</label>
</div>
</section>
@@ -582,6 +641,13 @@ onMounted(load)
gap: 12px;
}
.app-preview-brand {
padding: 6px 10px;
text-align: center;
font-size: 10px;
font-weight: 600;
}
@media (max-width: 768px) {
.palette-grid {
grid-template-columns: 1fr;