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:
@@ -31,6 +31,12 @@ export type HamkarUiConfig = {
|
|||||||
* Laravel settings stay source of truth (prevents visual jumps).
|
* Laravel settings stay source of truth (prevents visual jumps).
|
||||||
*/
|
*/
|
||||||
settingsManaged: boolean
|
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 */
|
/** Named CSS mode from live registry (Montpar, limangold, …) or dynamicMode */
|
||||||
registryMode: string
|
registryMode: string
|
||||||
brand: HamkarBrandColors
|
brand: HamkarBrandColors
|
||||||
@@ -103,6 +109,7 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig {
|
|||||||
return {
|
return {
|
||||||
kind: 'hamkar',
|
kind: 'hamkar',
|
||||||
settingsManaged: false,
|
settingsManaged: false,
|
||||||
|
brandManaged: false,
|
||||||
registryMode: 'dynamicMode',
|
registryMode: 'dynamicMode',
|
||||||
brand: {
|
brand: {
|
||||||
primaryColor: '',
|
primaryColor: '',
|
||||||
@@ -151,6 +158,7 @@ export function normalizeHamkarUiConfig(raw: unknown): HamkarUiConfig {
|
|||||||
return {
|
return {
|
||||||
kind: 'hamkar',
|
kind: 'hamkar',
|
||||||
settingsManaged: Boolean(input.settingsManaged),
|
settingsManaged: Boolean(input.settingsManaged),
|
||||||
|
brandManaged: Boolean(input.brandManaged),
|
||||||
registryMode,
|
registryMode,
|
||||||
brand,
|
brand,
|
||||||
settings,
|
settings,
|
||||||
@@ -177,6 +185,7 @@ export function hamkarUiToApiPayload(config: HamkarUiConfig) {
|
|||||||
return {
|
return {
|
||||||
kind: 'hamkar' as const,
|
kind: 'hamkar' as const,
|
||||||
settingsManaged: normalized.settingsManaged,
|
settingsManaged: normalized.settingsManaged,
|
||||||
|
brandManaged: normalized.brandManaged,
|
||||||
registryMode: normalized.registryMode,
|
registryMode: normalized.registryMode,
|
||||||
brand: {
|
brand: {
|
||||||
primaryColor: emptyToNull(normalized.brand.primaryColor),
|
primaryColor: emptyToNull(normalized.brand.primaryColor),
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ export function resolveHamkarEffectiveTheme(input: {
|
|||||||
bg2: panel,
|
bg2: panel,
|
||||||
colorPrimary: accent,
|
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).',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const palette = computed(() => ({
|
|||||||
const previewChrome = computed(() => {
|
const previewChrome = computed(() => {
|
||||||
const p = palette.value
|
const p = palette.value
|
||||||
const onHeader = textOn(p.header)
|
const onHeader = textOn(p.header)
|
||||||
|
const headerEdge =
|
||||||
|
onHeader === '#ffffff' ? '1px solid rgba(255,255,255,0.12)' : '1px solid #e2e8f0'
|
||||||
return {
|
return {
|
||||||
shell: {
|
shell: {
|
||||||
background: p.page,
|
background: p.page,
|
||||||
@@ -90,7 +92,12 @@ const previewChrome = computed(() => {
|
|||||||
header: {
|
header: {
|
||||||
background: p.header,
|
background: p.header,
|
||||||
color: onHeader,
|
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,
|
accent: p.accent,
|
||||||
muted: onHeader === '#ffffff' ? 'rgba(255,255,255,0.72)' : '#616161',
|
muted: onHeader === '#ffffff' ? 'rgba(255,255,255,0.72)' : '#616161',
|
||||||
@@ -221,6 +228,8 @@ async function save() {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
config.registryMode = 'dynamicMode'
|
config.registryMode = 'dynamicMode'
|
||||||
|
// Opt-in: live apps keep current chrome until colors are saved here.
|
||||||
|
config.brandManaged = true
|
||||||
if (!config.defaultSnapshot) {
|
if (!config.defaultSnapshot) {
|
||||||
ensureDefaultSnapshot(config, {
|
ensureDefaultSnapshot(config, {
|
||||||
notes: null,
|
notes: null,
|
||||||
@@ -259,6 +268,8 @@ function resetToDefault() {
|
|||||||
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.settings = { ...snap.settings }
|
config.settings = { ...snap.settings }
|
||||||
|
// Named factory modes must not keep brand chrome overrides.
|
||||||
|
config.brandManaged = snap.registryMode === 'dynamicMode'
|
||||||
await persist(hamkarUiToApiPayload(config))
|
await persist(hamkarUiToApiPayload(config))
|
||||||
seedEditableColors(config)
|
seedEditableColors(config)
|
||||||
message.success('ظاهر اولیه بازیابی شد')
|
message.success('ظاهر اولیه بازیابی شد')
|
||||||
@@ -294,9 +305,25 @@ onMounted(load)
|
|||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<section class="form-section preview">
|
<section class="form-section preview">
|
||||||
<h3>پیشنمایش</h3>
|
<h3>پیشنمایش موبایل</h3>
|
||||||
|
<p class="hint">هدر و منوی پایین یک رنگاند؛ بدنه صفحه جداست.</p>
|
||||||
<div class="app-preview" :style="previewChrome.shell">
|
<div class="app-preview" :style="previewChrome.shell">
|
||||||
<div class="app-preview-header" :style="previewChrome.header">
|
<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
|
<div
|
||||||
v-for="(item, i) in NAV_ITEMS"
|
v-for="(item, i) in NAV_ITEMS"
|
||||||
:key="item"
|
:key="item"
|
||||||
@@ -310,17 +337,6 @@ onMounted(load)
|
|||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -329,9 +345,12 @@ onMounted(load)
|
|||||||
<p v-if="hasDefaultSnapshot" class="hint">
|
<p v-if="hasDefaultSnapshot" class="hint">
|
||||||
ظاهر پیشفرض ثبتشده: <strong>{{ defaultModeLabel }}</strong>
|
ظاهر پیشفرض ثبتشده: <strong>{{ defaultModeLabel }}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
<p class="hint">
|
||||||
|
تا وقتی «ذخیره ظاهر همکار» نزنید، اپهای آنلاین عوض نمیشوند.
|
||||||
|
</p>
|
||||||
<div class="palette-grid">
|
<div class="palette-grid">
|
||||||
<label class="palette-card">
|
<label class="palette-card">
|
||||||
<span class="palette-title">هدر</span>
|
<span class="palette-title">هدر و منو</span>
|
||||||
<span class="palette-swatch" :style="{ background: palette.header }">
|
<span class="palette-swatch" :style="{ background: palette.header }">
|
||||||
<input
|
<input
|
||||||
:value="palette.header"
|
:value="palette.header"
|
||||||
@@ -365,7 +384,7 @@ onMounted(load)
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="palette-card">
|
<label class="palette-card">
|
||||||
<span class="palette-title">پسزمینه</span>
|
<span class="palette-title">بدنه صفحه</span>
|
||||||
<span class="palette-swatch" :style="{ background: palette.page }">
|
<span class="palette-swatch" :style="{ background: palette.page }">
|
||||||
<input
|
<input
|
||||||
:value="palette.page"
|
:value="palette.page"
|
||||||
@@ -469,12 +488,33 @@ onMounted(load)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-preview-header {
|
.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;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 10px 8px;
|
padding: 8px 6px 10px;
|
||||||
min-height: 56px;
|
min-height: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-preview-nav {
|
.app-preview-nav {
|
||||||
@@ -482,13 +522,13 @@ onMounted(load)
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-preview-dot {
|
.app-preview-dot {
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
@@ -497,6 +537,7 @@ onMounted(load)
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
min-height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-preview-card {
|
.app-preview-card {
|
||||||
|
|||||||
Reference in New Issue
Block a user