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 secondaryColor: string
/** Registry `background_color` → CSS `--bg` (dynamicMode) */ /** Registry `background_color` → CSS `--bg` (dynamicMode) */
backgroundColor: string backgroundColor: string
/** Menu / header icon stroke (brand_chrome) */
menuIconColor: string
/** Selected menu icon + label (brand_chrome) */
menuIconActiveColor: string
} }
export type HamkarUiSettings = { export type HamkarUiSettings = {
@@ -102,6 +106,8 @@ function normalizeBrand(raw: unknown): HamkarBrandColors {
primaryColor: asColor(brand.primaryColor), primaryColor: asColor(brand.primaryColor),
secondaryColor: asColor(brand.secondaryColor), secondaryColor: asColor(brand.secondaryColor),
backgroundColor: asColor(brand.backgroundColor), backgroundColor: asColor(brand.backgroundColor),
menuIconColor: asColor(brand.menuIconColor),
menuIconActiveColor: asColor(brand.menuIconActiveColor),
} }
} }
@@ -115,6 +121,8 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig {
primaryColor: '', primaryColor: '',
secondaryColor: '', secondaryColor: '',
backgroundColor: '', backgroundColor: '',
menuIconColor: '',
menuIconActiveColor: '',
}, },
settings: { settings: {
pricing_box_theme: '1', pricing_box_theme: '1',
@@ -177,6 +185,10 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
primaryColor: emptyToNull(normalized.defaultSnapshot.brand.primaryColor), primaryColor: emptyToNull(normalized.defaultSnapshot.brand.primaryColor),
secondaryColor: emptyToNull(normalized.defaultSnapshot.brand.secondaryColor), secondaryColor: emptyToNull(normalized.defaultSnapshot.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.defaultSnapshot.brand.backgroundColor), backgroundColor: emptyToNull(normalized.defaultSnapshot.brand.backgroundColor),
menuIconColor: emptyToNull(normalized.defaultSnapshot.brand.menuIconColor),
menuIconActiveColor: emptyToNull(
normalized.defaultSnapshot.brand.menuIconActiveColor,
),
}, },
settings: { ...normalized.defaultSnapshot.settings }, settings: { ...normalized.defaultSnapshot.settings },
} }
@@ -191,6 +203,8 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
primaryColor: emptyToNull(normalized.brand.primaryColor), primaryColor: emptyToNull(normalized.brand.primaryColor),
secondaryColor: emptyToNull(normalized.brand.secondaryColor), secondaryColor: emptyToNull(normalized.brand.secondaryColor),
backgroundColor: emptyToNull(normalized.brand.backgroundColor), backgroundColor: emptyToNull(normalized.brand.backgroundColor),
menuIconColor: emptyToNull(normalized.brand.menuIconColor),
menuIconActiveColor: emptyToNull(normalized.brand.menuIconActiveColor),
}, },
settings: { ...normalized.settings }, settings: { ...normalized.settings },
...(snap ? { defaultSnapshot: snap } : {}), ...(snap ? { defaultSnapshot: snap } : {}),
+69 -3
View File
@@ -75,6 +75,8 @@ const palette = computed(() => ({
header: swatchHex(config.brand.primaryColor, '#ffffff'), header: swatchHex(config.brand.primaryColor, '#ffffff'),
accent: swatchHex(config.brand.secondaryColor, '#2563EB'), accent: swatchHex(config.brand.secondaryColor, '#2563EB'),
page: swatchHex(config.brand.backgroundColor, '#fafafa'), 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(() => { const previewChrome = computed(() => {
@@ -99,8 +101,14 @@ const previewChrome = computed(() => {
color: onHeader, color: onHeader,
borderTop: headerEdge, borderTop: headerEdge,
}, },
brandFooter: {
background: p.header,
color: p.menuIcon,
},
accent: p.accent, 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.secondaryColor) normalized.brand.secondaryColor = effective.accent
if (!normalized.brand.backgroundColor) normalized.brand.backgroundColor = effective.pageBg 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( function ensureDefaultSnapshot(
@@ -148,6 +164,8 @@ function ensureDefaultSnapshot(
primaryColor: String(opts.primaryColor || normalized.brand.primaryColor || ''), primaryColor: String(opts.primaryColor || normalized.brand.primaryColor || ''),
secondaryColor: String(opts.secondaryColor || normalized.brand.secondaryColor || ''), secondaryColor: String(opts.secondaryColor || normalized.brand.secondaryColor || ''),
backgroundColor: String(opts.backgroundColor || normalized.brand.backgroundColor || ''), backgroundColor: String(opts.backgroundColor || normalized.brand.backgroundColor || ''),
menuIconColor: String(normalized.brand.menuIconColor || ''),
menuIconActiveColor: String(normalized.brand.menuIconActiveColor || ''),
}, },
settings: { ...normalized.settings }, settings: { ...normalized.settings },
}) })
@@ -267,6 +285,8 @@ function resetToDefault() {
config.brand.primaryColor = snap.brand.primaryColor config.brand.primaryColor = snap.brand.primaryColor
config.brand.secondaryColor = snap.brand.secondaryColor config.brand.secondaryColor = snap.brand.secondaryColor
config.brand.backgroundColor = snap.brand.backgroundColor config.brand.backgroundColor = snap.brand.backgroundColor
config.brand.menuIconColor = snap.brand.menuIconColor
config.brand.menuIconActiveColor = snap.brand.menuIconActiveColor
config.settings = { ...snap.settings } config.settings = { ...snap.settings }
// Named factory modes must not keep brand chrome overrides. // Named factory modes must not keep brand chrome overrides.
config.brandManaged = snap.registryMode === 'dynamicMode' config.brandManaged = snap.registryMode === 'dynamicMode'
@@ -328,15 +348,20 @@ onMounted(load)
v-for="(item, i) in NAV_ITEMS" v-for="(item, i) in NAV_ITEMS"
:key="item" :key="item"
class="app-preview-nav" class="app-preview-nav"
:style="{ color: i === 0 ? previewChrome.accent : previewChrome.muted }" :style="{ color: i === 0 ? previewChrome.menuIconActive : previewChrome.menuIcon }"
> >
<span <span
class="app-preview-dot" class="app-preview-dot"
:style="{ background: i === 0 ? previewChrome.accent : previewChrome.muted }" :style="{
background: i === 0 ? previewChrome.menuIconActive : previewChrome.menuIcon,
}"
/> />
{{ item }} {{ item }}
</div> </div>
</div> </div>
<div class="app-preview-brand" :style="previewChrome.brandFooter">
توسعه و طراحی توسط
</div>
</div> </div>
</section> </section>
@@ -399,6 +424,40 @@ onMounted(load)
class="palette-hex" class="palette-hex"
/> />
</label> </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> </div>
</section> </section>
@@ -582,6 +641,13 @@ onMounted(load)
gap: 12px; gap: 12px;
} }
.app-preview-brand {
padding: 6px 10px;
text-align: center;
font-size: 10px;
font-weight: 600;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.palette-grid { .palette-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;