feat(hamkar-ui): mobile preview and brandManaged for chrome colors

Save sets brandManaged so header+menu share one color and body stays separate, without changing live apps until save.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 17:04:33 +03:30
co-authored by Cursor
parent f90df62ee0
commit 71e6655f90
3 changed files with 71 additions and 21 deletions
+9
View File
@@ -31,6 +31,12 @@ export type HamkarUiConfig = {
* Laravel settings stay source of truth (prevents visual jumps).
*/
settingsManaged: boolean
/**
* When true, Hamkar applies brand colors to mobile chrome (header + bottom menu)
* and page body — including theme-two. Default false so live apps stay untouched
* until colors are saved from SuperAdmin.
*/
brandManaged: boolean
/** Named CSS mode from live registry (Montpar, limangold, …) or dynamicMode */
registryMode: string
brand: HamkarBrandColors
@@ -103,6 +109,7 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig {
return {
kind: 'hamkar',
settingsManaged: false,
brandManaged: false,
registryMode: 'dynamicMode',
brand: {
primaryColor: '',
@@ -151,6 +158,7 @@ export function normalizeHamkarUiConfig(raw: unknown): HamkarUiConfig {
return {
kind: 'hamkar',
settingsManaged: Boolean(input.settingsManaged),
brandManaged: Boolean(input.brandManaged),
registryMode,
brand,
settings,
@@ -177,6 +185,7 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
return {
kind: 'hamkar' as const,
settingsManaged: normalized.settingsManaged,
brandManaged: normalized.brandManaged,
registryMode: normalized.registryMode,
brand: {
primaryColor: emptyToNull(normalized.brand.primaryColor),
+1 -1
View File
@@ -75,6 +75,6 @@ export function resolveHamkarEffectiveTheme(input: {
bg2: panel,
colorPrimary: accent,
},
note: 'حالت dynamicMode: color → هدر (--bg2)، secondary_color → اکسنت (--colorPrimary)، background_color → پس‌زمینه (--bg).',
note: 'حالت dynamicMode: color → هدر و منوی موبایل (--bg2 / --theme-two-header)، secondary_color → اکسنت (--colorPrimary)، background_color → بدنه صفحه (--bg / --theme-two-background).',
}
}
+61 -20
View File
@@ -80,6 +80,8 @@ const palette = computed(() => ({
const previewChrome = computed(() => {
const p = palette.value
const onHeader = textOn(p.header)
const headerEdge =
onHeader === '#ffffff' ? '1px solid rgba(255,255,255,0.12)' : '1px solid #e2e8f0'
return {
shell: {
background: p.page,
@@ -90,7 +92,12 @@ const previewChrome = computed(() => {
header: {
background: p.header,
color: onHeader,
borderBottom: onHeader === '#ffffff' ? '1px solid rgba(255,255,255,0.12)' : '1px solid #e2e8f0',
borderBottom: headerEdge,
},
footer: {
background: p.header,
color: onHeader,
borderTop: headerEdge,
},
accent: p.accent,
muted: onHeader === '#ffffff' ? 'rgba(255,255,255,0.72)' : '#616161',
@@ -221,6 +228,8 @@ async function save() {
saving.value = true
try {
config.registryMode = 'dynamicMode'
// Opt-in: live apps keep current chrome until colors are saved here.
config.brandManaged = true
if (!config.defaultSnapshot) {
ensureDefaultSnapshot(config, {
notes: null,
@@ -259,6 +268,8 @@ function resetToDefault() {
config.brand.secondaryColor = snap.brand.secondaryColor
config.brand.backgroundColor = snap.brand.backgroundColor
config.settings = { ...snap.settings }
// Named factory modes must not keep brand chrome overrides.
config.brandManaged = snap.registryMode === 'dynamicMode'
await persist(hamkarUiToApiPayload(config))
seedEditableColors(config)
message.success('ظاهر اولیه بازیابی شد')
@@ -294,9 +305,25 @@ onMounted(load)
<a-spin :spinning="loading">
<div class="layout">
<section class="form-section preview">
<h3>پیش‌نمایش</h3>
<h3>پیش‌نمایش موبایل</h3>
<p class="hint">هدر و منوی پایین یک رنگ‌اند؛ بدنه صفحه جداست.</p>
<div class="app-preview" :style="previewChrome.shell">
<div class="app-preview-header" :style="previewChrome.header">
<span class="app-preview-user">علی منتظر</span>
<span class="app-preview-logo" :style="{ background: previewChrome.accent }" />
</div>
<div class="app-preview-body">
<div class="app-preview-card">
<div class="app-preview-card-title">خوش آمدگویی</div>
<div class="app-preview-card-line" />
<div class="app-preview-card-line short" />
</div>
<div class="app-preview-price" :style="{ borderColor: palette.accent }">
<span>نقد فردا</span>
<strong :style="{ color: palette.accent }">۸۱٬۴۵۰٬۰۰۰</strong>
</div>
</div>
<div class="app-preview-footer" :style="previewChrome.footer">
<div
v-for="(item, i) in NAV_ITEMS"
:key="item"
@@ -310,17 +337,6 @@ onMounted(load)
{{ item }}
</div>
</div>
<div class="app-preview-body">
<div class="app-preview-card">
<div class="app-preview-card-title">خوش آمدگویی</div>
<div class="app-preview-card-line" />
<div class="app-preview-card-line short" />
</div>
<div class="app-preview-price" :style="{ borderColor: palette.accent }">
<span>نقد فردا</span>
<strong :style="{ color: palette.accent }">۸۱٬۴۵۰٬۰۰۰</strong>
</div>
</div>
</div>
</section>
@@ -329,9 +345,12 @@ onMounted(load)
<p v-if="hasDefaultSnapshot" class="hint">
ظاهر پیش‌فرض ثبت‌شده: <strong>{{ defaultModeLabel }}</strong>
</p>
<p class="hint">
تا وقتی «ذخیره ظاهر همکار» نزنید، اپ‌های آنلاین عوض نمی‌شوند.
</p>
<div class="palette-grid">
<label class="palette-card">
<span class="palette-title">هدر</span>
<span class="palette-title">هدر و منو</span>
<span class="palette-swatch" :style="{ background: palette.header }">
<input
:value="palette.header"
@@ -365,7 +384,7 @@ onMounted(load)
</label>
<label class="palette-card">
<span class="palette-title">پس‌زمینه</span>
<span class="palette-title">بدنه صفحه</span>
<span class="palette-swatch" :style="{ background: palette.page }">
<input
:value="palette.page"
@@ -469,12 +488,33 @@ onMounted(load)
}
.app-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
padding: 12px 14px;
min-height: 52px;
}
.app-preview-user {
font-size: 13px;
font-weight: 700;
}
.app-preview-logo {
width: 28px;
height: 28px;
border-radius: 8px;
flex-shrink: 0;
}
.app-preview-footer {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 4px;
padding: 10px 8px;
min-height: 56px;
padding: 8px 6px 10px;
min-height: 58px;
}
.app-preview-nav {
@@ -482,13 +522,13 @@ onMounted(load)
flex-direction: column;
align-items: center;
gap: 4px;
font-size: 11px;
font-size: 10px;
font-weight: 700;
}
.app-preview-dot {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
border-radius: 6px;
opacity: 0.9;
}
@@ -497,6 +537,7 @@ onMounted(load)
padding: 14px;
display: grid;
gap: 10px;
min-height: 160px;
}
.app-preview-card {