2160 lines
131 KiB
JavaScript
2160 lines
131 KiB
JavaScript
figma.showUI(__html__, { width: 420, height: 700, themeColors: true });
|
||
|
||
const W = 1440;
|
||
const H = 1024;
|
||
const NAV = 278;
|
||
const TOP = 96;
|
||
const GAP = 28;
|
||
const CONTENT_X = NAV + 32;
|
||
const CONTENT_W = W - NAV - 64;
|
||
|
||
const C = {
|
||
ink: hex('#1B1018'),
|
||
ink2: hex('#2A1724'),
|
||
wine900: hex('#3A0B1D'),
|
||
wine800: hex('#551128'),
|
||
wine700: hex('#741838'),
|
||
wine600: hex('#8E2149'),
|
||
wine500: hex('#A8325C'),
|
||
rose: hex('#D69A8C'),
|
||
rose2: hex('#E8B9AD'),
|
||
roseSoft: hex('#F8E9E5'),
|
||
gold: hex('#D8B35F'),
|
||
gold2: hex('#F1D88A'),
|
||
goldDark: hex('#8B661D'),
|
||
cream: hex('#FBF6EF'),
|
||
cream2: hex('#F4E7DA'),
|
||
champagne: hex('#EFE1D0'),
|
||
surface: hex('#FFFFFF'),
|
||
surface2: hex('#FFFDFC'),
|
||
text: hex('#231620'),
|
||
text2: hex('#5C4452'),
|
||
muted: hex('#8F7884'),
|
||
faint: hex('#B9A5AE'),
|
||
border: hex('#E9DCD4'),
|
||
borderWine: hex('#E4C7D1'),
|
||
success: hex('#2D8B62'),
|
||
successSoft: hex('#EAF7F0'),
|
||
warning: hex('#B87918'),
|
||
warningSoft: hex('#FFF4D8'),
|
||
danger: hex('#B74343'),
|
||
dangerSoft: hex('#FCEAEA'),
|
||
blue: hex('#5266D6'),
|
||
blueSoft: hex('#EBEEFF'),
|
||
violet: hex('#7B61D1'),
|
||
violetSoft: hex('#F0ECFF')
|
||
};
|
||
|
||
const NAV_ITEMS = [
|
||
['Overview', 'overview'],
|
||
['Calendar', 'calendar'],
|
||
['Appointments', 'calendar'],
|
||
['Clients', 'clients'],
|
||
['Staff', 'staff'],
|
||
['Services', 'services'],
|
||
['Inventory', 'inventory'],
|
||
['Orders', 'orders'],
|
||
['Store', 'store'],
|
||
['Reports', 'analytics'],
|
||
['Marketing', 'marketing'],
|
||
['Reviews', 'reviews'],
|
||
['Payments', 'payments'],
|
||
['Settings', 'settings']
|
||
];
|
||
|
||
const SCREEN_COUNT_LABEL = '33 real web interfaces';
|
||
const PRODUCT_SCREEN_COUNT = 30;
|
||
|
||
const SCREENS = [
|
||
{ title: 'Onboarding Introduction', path: '/onboarding', subtitle: 'Educational product tour with luxury salon imagery, benefits, and setup guidance.', mode: 'auth', build: pageOnboarding },
|
||
{ title: 'Login Portal', path: '/login', subtitle: 'Secure web sign-in experience for salon owners, managers, and staff.', mode: 'auth', build: pageLogin },
|
||
{ title: 'Register Salon Account', path: '/register', subtitle: 'Premium account creation flow for beauty centers and salon groups.', mode: 'auth', build: pageRegister },
|
||
|
||
{ title: 'Main Dashboard', path: '/dashboard', subtitle: 'Executive command center for revenue, bookings, clients, and salon health.', mode: 'dashboard', active: 0, build: pageOverview },
|
||
{ title: 'Calendar Dashboard', path: '/dashboard/calendar', subtitle: 'Resource calendar for chairs, rooms, specialists, and VIP booking flow.', mode: 'dashboard', active: 1, build: pageCalendar },
|
||
{ title: 'Appointments Board', path: '/dashboard/appointments', subtitle: 'Pipeline view for upcoming, waiting, checked-in, completed, and cancelled appointments.', mode: 'dashboard', active: 2, build: pageAppointmentsBoard },
|
||
{ title: 'Appointment Details', path: '/dashboard/appointments/details', subtitle: 'Full appointment profile with services, deposits, client notes, and timeline.', mode: 'dashboard', active: 2, build: pageAppointmentDetails },
|
||
{ title: 'Add Appointment Modal', path: '/dashboard/appointments/new', subtitle: 'Booking creation state with modal overlay, service picker, and specialist availability.', mode: 'dashboard', active: 2, build: pageAddAppointmentModal },
|
||
|
||
{ title: 'Clients Dashboard', path: '/dashboard/clients', subtitle: 'High-touch client intelligence, segments, loyalty, and history.', mode: 'dashboard', active: 3, build: pageClientCRM },
|
||
{ title: 'Client Profile', path: '/dashboard/clients/profile', subtitle: 'Single guest profile with preferences, spend, visits, timeline, and relationship notes.', mode: 'dashboard', active: 3, build: pageClientProfile },
|
||
{ title: 'Client Medical Record', path: '/dashboard/clients/medical-record', subtitle: 'Secure beauty and wellness record for allergies, contraindications, treatments, and consent.', mode: 'dashboard', active: 3, build: pageClientMedicalRecord },
|
||
{ title: 'Add Client Modal', path: '/dashboard/clients/new', subtitle: 'Client creation state with contact details, preferences, tags, and consent fields.', mode: 'dashboard', active: 3, build: pageAddClientModal },
|
||
|
||
{ title: 'Staff Dashboard', path: '/dashboard/staff', subtitle: 'Team performance, commission, attendance, and specialist planning.', mode: 'dashboard', active: 4, build: pageStaff },
|
||
{ title: 'Staff Schedule', path: '/dashboard/staff/schedule', subtitle: 'Weekly rota with shifts, breaks, rooms, leave requests, and capacity planning.', mode: 'dashboard', active: 4, build: pageStaffSchedule },
|
||
{ title: 'Employee Profile', path: '/dashboard/staff/profile', subtitle: 'Specialist profile with services, certifications, reviews, targets, and payroll details.', mode: 'dashboard', active: 4, build: pageEmployeeProfile },
|
||
|
||
{ title: 'Services Board', path: '/dashboard/services', subtitle: 'Service catalog, packages, add-ons, duration, margins, and pricing strategy.', mode: 'dashboard', active: 5, build: pageServices },
|
||
{ title: 'Service Details', path: '/dashboard/services/details', subtitle: 'Single service page with price rules, add-ons, specialists, instructions, and profitability.', mode: 'dashboard', active: 5, build: pageServiceDetails },
|
||
|
||
{ title: 'Inventory Dashboard', path: '/dashboard/inventory', subtitle: 'Stock intelligence, reorder alerts, product shelves, and retail revenue.', mode: 'dashboard', active: 6, build: pageInventory },
|
||
{ title: 'Add Product Modal', path: '/dashboard/inventory/products/new', subtitle: 'Product creation state for SKU, vendor, stock level, retail price, and reorder thresholds.', mode: 'dashboard', active: 6, build: pageAddProductModal },
|
||
|
||
{ title: 'Orders Dashboard', path: '/dashboard/orders', subtitle: 'Purchase orders, retail orders, fulfillment status, vendors, and returns.', mode: 'dashboard', active: 7, build: pageOrdersDashboard },
|
||
{ title: 'Store Dashboard', path: '/dashboard/store', subtitle: 'Retail storefront operations with featured products, carts, shelves, and conversion metrics.', mode: 'dashboard', active: 8, build: pageStoreDashboard },
|
||
{ title: 'Reports Dashboard', path: '/dashboard/reports', subtitle: 'Operational reporting hub with exports, filters, saved views, and executive summaries.', mode: 'dashboard', active: 9, build: pageReportsDashboard },
|
||
{ title: 'Revenue Analytics', path: '/dashboard/reports/revenue', subtitle: 'Revenue analytics with channel mix, cohorts, average ticket, and forecasted sales.', mode: 'dashboard', active: 9, build: pageRevenueAnalytics },
|
||
|
||
{ title: 'Marketing Dashboard', path: '/dashboard/marketing', subtitle: 'Campaign automation, audiences, creative previews, and growth funnels.', mode: 'dashboard', active: 10, build: pageMarketing },
|
||
{ title: 'Loyalty Program', path: '/dashboard/marketing/loyalty', subtitle: 'Points, tiers, rewards, referrals, and retention program management.', mode: 'dashboard', active: 10, build: pageLoyaltyProgram },
|
||
{ title: 'Reviews Dashboard', path: '/dashboard/reviews', subtitle: 'Ratings, sentiment, channel performance, and response workflows.', mode: 'dashboard', active: 11, build: pageReviews },
|
||
{ title: 'Notification Center', path: '/dashboard/notifications', subtitle: 'Dedicated UI state for the notification bell with alerts, filters, and action cards.', mode: 'dashboard', active: 0, build: pageNotificationCenter },
|
||
{ title: 'Messages / Chat', path: '/dashboard/messages', subtitle: 'Client messaging workspace with conversations, templates, notes, and booking actions.', mode: 'dashboard', active: 3, build: pageMessagesChat },
|
||
|
||
{ title: 'Payments Dashboard', path: '/dashboard/payments', subtitle: 'Transactions, deposits, settlement, invoices, refunds, and payment methods.', mode: 'dashboard', active: 12, build: pagePayments },
|
||
{ title: 'Subscription Plans', path: '/dashboard/billing/subscription', subtitle: 'Plan comparison, billing usage, invoices, payment method, and upgrade controls.', mode: 'dashboard', active: 12, build: pageSubscriptionPlans },
|
||
{ title: 'Settings Dashboard', path: '/dashboard/settings', subtitle: 'Brand system, permissions, integrations, billing, and business controls.', mode: 'dashboard', active: 13, build: pageSettings },
|
||
{ title: 'User Profile', path: '/dashboard/profile', subtitle: 'Operator profile with role, access, activity, preferences, and security state.', mode: 'dashboard', active: 13, build: pageUserProfile },
|
||
{ title: 'Edit Profile Modal', path: '/dashboard/profile/edit', subtitle: 'Profile editing state with modal overlay, avatar controls, personal data, and preferences.', mode: 'dashboard', active: 13, build: pageEditProfileModal }
|
||
];
|
||
|
||
const fontCache = {};
|
||
|
||
figma.ui.onmessage = async (msg) => {
|
||
if (!msg || (msg.type !== 'generate-premium-dashboard' && msg.type !== 'generate')) return;
|
||
try {
|
||
await generatePremiumDashboards();
|
||
} catch (error) {
|
||
const message = error && error.message ? error.message : 'Unknown generation error.';
|
||
figma.ui.postMessage({ type: 'error', message });
|
||
figma.notify('Generation failed: ' + message, { error: true });
|
||
}
|
||
};
|
||
|
||
async function generatePremiumDashboards() {
|
||
await preloadFonts();
|
||
|
||
const page = figma.createPage();
|
||
page.name = uniquePageName('BeautyHub Premium Web Interface System');
|
||
figma.currentPage = page;
|
||
|
||
const frames = [];
|
||
for (let i = 0; i < SCREENS.length; i++) {
|
||
const screen = SCREENS[i];
|
||
figma.ui.postMessage({ type: 'progress', current: i, total: SCREENS.length, label: 'Building ' + screen.title + '...' });
|
||
|
||
const mainFrame = screen.mode === 'auth'
|
||
? await baseAuthFrame(i + 1, screen.title, screen.subtitle, screen.path)
|
||
: await baseDashboardFrame(i + 1, screen.title, screen.subtitle, screen.active, screen.path);
|
||
|
||
await screen.build(mainFrame);
|
||
frames.push(mainFrame);
|
||
|
||
figma.ui.postMessage({ type: 'progress', current: i + 1, total: SCREENS.length, label: screen.title + ' interface complete' });
|
||
}
|
||
|
||
figma.viewport.scrollAndZoomIntoView(frames);
|
||
figma.notify('Generated ' + SCREENS.length + ' premium BeautyHub web interfaces. No extra detail frames.');
|
||
figma.ui.postMessage({ type: 'done', total: SCREENS.length, detailTotal: 0, frameTotal: frames.length, productScreens: PRODUCT_SCREEN_COUNT });
|
||
}
|
||
|
||
function uniquePageName(base) {
|
||
const names = figma.root.children.map((p) => p.name);
|
||
if (!names.includes(base)) return base;
|
||
let n = 2;
|
||
while (names.includes(base + ' ' + n)) n += 1;
|
||
return base + ' ' + n;
|
||
}
|
||
|
||
function screenGridPosition(index) {
|
||
const columns = 4;
|
||
const col = (index - 1) % columns;
|
||
const row = Math.floor((index - 1) / columns);
|
||
return { x: col * (W + 88), y: row * (H + 88) };
|
||
}
|
||
|
||
async function preloadFonts() {
|
||
await Promise.all([
|
||
getFont('display', 'regular'), getFont('display', 'semibold'), getFont('display', 'bold'), getFont('display', 'heavy'),
|
||
getFont('text', 'regular'), getFont('text', 'medium'), getFont('text', 'semibold'), getFont('text', 'bold')
|
||
]);
|
||
}
|
||
|
||
async function getFont(role, weight) {
|
||
const key = role + '-' + weight;
|
||
if (fontCache[key]) return fontCache[key];
|
||
|
||
const families = role === 'display'
|
||
? ['SF Pro Display', 'SF Pro Text', 'Inter', 'Roboto']
|
||
: ['SF Pro Text', 'SF Pro Display', 'Inter', 'Roboto'];
|
||
|
||
const styles = {
|
||
regular: ['Regular'],
|
||
medium: ['Medium', 'Regular'],
|
||
semibold: ['Semibold', 'Semi Bold', 'Medium', 'Bold'],
|
||
bold: ['Bold', 'Semibold', 'Semi Bold'],
|
||
heavy: ['Heavy', 'Black', 'Bold', 'Semibold']
|
||
}[weight] || ['Regular'];
|
||
|
||
for (const family of families) {
|
||
for (const style of styles) {
|
||
try {
|
||
const font = { family, style };
|
||
await figma.loadFontAsync(font);
|
||
fontCache[key] = font;
|
||
return font;
|
||
} catch (err) {
|
||
// Keep trying fallbacks. The design targets SF Pro and safely falls back if unavailable in a Figma environment.
|
||
}
|
||
}
|
||
}
|
||
|
||
const fallback = { family: 'Inter', style: 'Regular' };
|
||
await figma.loadFontAsync(fallback);
|
||
fontCache[key] = fallback;
|
||
return fallback;
|
||
}
|
||
|
||
function hex(value) {
|
||
const v = value.replace('#', '');
|
||
return {
|
||
r: parseInt(v.slice(0, 2), 16) / 255,
|
||
g: parseInt(v.slice(2, 4), 16) / 255,
|
||
b: parseInt(v.slice(4, 6), 16) / 255
|
||
};
|
||
}
|
||
|
||
function rgba(color, a = 1) {
|
||
return { r: color.r, g: color.g, b: color.b, a };
|
||
}
|
||
|
||
function solid(color, opacity = 1) {
|
||
return [{ type: 'SOLID', color, opacity }];
|
||
}
|
||
|
||
function gradient(stops, direction = 'diagonal') {
|
||
const transforms = {
|
||
horizontal: [[1, 0, 0], [0, 1, 0]],
|
||
vertical: [[0, 1, 0], [-1, 0, 1]],
|
||
diagonal: [[0.78, 0.63, -0.12], [-0.63, 0.78, 0.42]],
|
||
reverse: [[-0.78, 0.63, 0.9], [-0.63, -0.78, 1.1]]
|
||
};
|
||
return [{
|
||
type: 'GRADIENT_LINEAR',
|
||
gradientTransform: transforms[direction] || transforms.diagonal,
|
||
gradientStops: stops.map((stop) => ({ position: stop[0], color: rgba(stop[1], stop[2] === undefined ? 1 : stop[2]) }))
|
||
}];
|
||
}
|
||
|
||
function effects(kind = 'card') {
|
||
if (kind === 'soft') {
|
||
return [
|
||
{ type: 'DROP_SHADOW', color: rgba(C.wine900, 0.06), offset: { x: 0, y: 10 }, radius: 24, spread: 0, visible: true, blendMode: 'NORMAL' }
|
||
];
|
||
}
|
||
if (kind === 'hero') {
|
||
return [
|
||
{ type: 'DROP_SHADOW', color: rgba(C.wine900, 0.20), offset: { x: 0, y: 26 }, radius: 58, spread: -8, visible: true, blendMode: 'NORMAL' },
|
||
{ type: 'DROP_SHADOW', color: rgba(C.gold, 0.10), offset: { x: 0, y: 8 }, radius: 24, spread: -8, visible: true, blendMode: 'NORMAL' }
|
||
];
|
||
}
|
||
if (kind === 'glass') {
|
||
return [
|
||
{ type: 'DROP_SHADOW', color: rgba(C.wine900, 0.10), offset: { x: 0, y: 16 }, radius: 34, spread: -10, visible: true, blendMode: 'NORMAL' },
|
||
{ type: 'BACKGROUND_BLUR', radius: 18, visible: true }
|
||
];
|
||
}
|
||
return [
|
||
{ type: 'DROP_SHADOW', color: rgba(C.wine900, 0.10), offset: { x: 0, y: 18 }, radius: 42, spread: -12, visible: true, blendMode: 'NORMAL' },
|
||
{ type: 'DROP_SHADOW', color: rgba(C.wine900, 0.04), offset: { x: 0, y: 2 }, radius: 8, spread: 0, visible: true, blendMode: 'NORMAL' }
|
||
];
|
||
}
|
||
|
||
function addStroke(node, color = C.border, opacity = 1, weight = 1) {
|
||
node.strokes = [{ type: 'SOLID', color, opacity }];
|
||
node.strokeWeight = weight;
|
||
node.strokeAlign = 'INSIDE';
|
||
}
|
||
|
||
function rect(parent, x, y, w, h, color, opacity = 1, radius = 0, name = 'Rectangle') {
|
||
const node = figma.createRectangle();
|
||
parent.appendChild(node);
|
||
node.name = name;
|
||
node.x = x;
|
||
node.y = y;
|
||
node.resize(w, h);
|
||
node.fills = solid(color, opacity);
|
||
node.cornerRadius = radius;
|
||
return node;
|
||
}
|
||
|
||
function ellipse(parent, x, y, w, h, color, opacity = 1, name = 'Ellipse') {
|
||
const node = figma.createEllipse();
|
||
parent.appendChild(node);
|
||
node.name = name;
|
||
node.x = x;
|
||
node.y = y;
|
||
node.resize(w, h);
|
||
node.fills = solid(color, opacity);
|
||
return node;
|
||
}
|
||
|
||
function frame(parent, x, y, w, h, color = C.surface, opacity = 1, radius = 0, name = 'Frame') {
|
||
const node = figma.createFrame();
|
||
parent.appendChild(node);
|
||
node.name = name;
|
||
node.x = x;
|
||
node.y = y;
|
||
node.resize(w, h);
|
||
node.fills = solid(color, opacity);
|
||
node.cornerRadius = radius;
|
||
node.clipsContent = false;
|
||
return node;
|
||
}
|
||
|
||
function line(parent, x1, y1, x2, y2, color = C.wine600, opacity = 1, thickness = 2, name = 'Line') {
|
||
const dx = x2 - x1;
|
||
const dy = y2 - y1;
|
||
const len = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
|
||
const node = rect(parent, x1, y1 - thickness / 2, len, thickness, color, opacity, thickness / 2, name);
|
||
node.rotation = Math.atan2(dy, dx) * 180 / Math.PI;
|
||
return node;
|
||
}
|
||
|
||
async function txt(parent, x, y, w, h, value, size = 14, color = C.text, weight = 'regular', align = 'LEFT', role = 'text', opacity = 1, valign = 'CENTER') {
|
||
const node = figma.createText();
|
||
parent.appendChild(node);
|
||
node.name = value.length > 28 ? value.slice(0, 28) + '...' : value;
|
||
node.x = x;
|
||
node.y = y;
|
||
node.resize(w, h);
|
||
node.fontName = await getFont(role, weight);
|
||
node.characters = value;
|
||
node.fontSize = size;
|
||
node.fills = solid(color, opacity);
|
||
node.textAlignHorizontal = align;
|
||
node.textAlignVertical = valign;
|
||
node.lineHeight = { unit: 'PERCENT', value: size >= 24 ? 108 : 132 };
|
||
node.letterSpacing = { unit: 'PERCENT', value: size >= 24 ? -2.4 : -0.6 };
|
||
return node;
|
||
}
|
||
|
||
async function baseDashboardFrame(index, title, subtitle, active, path) {
|
||
const root = figma.createFrame();
|
||
figma.currentPage.appendChild(root);
|
||
root.name = String(index).padStart(2, '0') + ' · ' + title;
|
||
const pos = screenGridPosition(index);
|
||
root.x = pos.x;
|
||
root.y = pos.y;
|
||
root.resize(W, H);
|
||
root.clipsContent = true;
|
||
root.fills = gradient([
|
||
[0, C.cream, 1],
|
||
[0.48, C.surface2, 1],
|
||
[1, C.cream2, 1]
|
||
], 'diagonal');
|
||
|
||
addAmbient(root);
|
||
await sidebar(root, active);
|
||
await topbar(root, title, subtitle, path);
|
||
return root;
|
||
}
|
||
|
||
function addAmbient(root) {
|
||
ellipse(root, NAV + 80, -180, 520, 520, C.rose, 0.17, 'Rose ambient glow');
|
||
ellipse(root, W - 330, -120, 460, 460, C.gold, 0.13, 'Gold ambient glow');
|
||
ellipse(root, W - 420, H - 220, 560, 360, C.wine500, 0.07, 'Wine ambient glow');
|
||
}
|
||
|
||
async function sidebar(root, active) {
|
||
const sb = frame(root, 0, 0, NAV, H, C.ink, 1, 0, 'Luxury sidebar');
|
||
sb.fills = gradient([
|
||
[0, C.ink, 1],
|
||
[0.48, C.wine900, 1],
|
||
[1, C.wine800, 1]
|
||
], 'vertical');
|
||
|
||
ellipse(sb, -110, -100, 260, 260, C.gold, 0.18, 'Sidebar gold glow');
|
||
ellipse(sb, 130, 690, 240, 240, C.rose, 0.11, 'Sidebar rose glow');
|
||
|
||
const mark = frame(sb, 28, 28, 54, 54, C.surface, 0.12, 18, 'Brand mark');
|
||
mark.fills = gradient([[0, C.wine500, 1], [0.55, C.rose, 1], [1, C.gold, 1]], 'diagonal');
|
||
mark.effects = effects('soft');
|
||
await txt(mark, 0, 0, 54, 54, 'BH', 18, C.surface, 'heavy', 'CENTER', 'display');
|
||
await txt(sb, 94, 30, 150, 22, 'BeautyHub', 20, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(sb, 94, 56, 150, 18, 'SalonOS Dashboard', 11, C.rose2, 'medium', 'LEFT', 'text', 0.9);
|
||
|
||
await txt(sb, 28, 112, 180, 18, 'MANAGEMENT', 10, C.rose2, 'bold', 'LEFT', 'text', 0.7);
|
||
|
||
for (let i = 0; i < NAV_ITEMS.length; i++) {
|
||
const y = 136 + i * 44;
|
||
const isActive = i === active;
|
||
if (isActive) {
|
||
const bg = frame(sb, 18, y - 4, NAV - 36, 38, C.surface, 0.14, 15, 'Active nav item');
|
||
bg.effects = effects('glass');
|
||
addStroke(bg, C.surface, 0.12);
|
||
rect(sb, 18, y + 2, 4, 24, C.gold, 1, 4, 'Active accent');
|
||
}
|
||
iconChip(sb, 32, y, NAV_ITEMS[i][1], isActive ? C.gold : C.rose2, isActive ? C.surface : C.surface, isActive ? 0.16 : 0.07, 28);
|
||
await txt(sb, 72, y - 1, 150, 30, NAV_ITEMS[i][0], 12, isActive ? C.surface : C.rose2, isActive ? 'semibold' : 'medium', 'LEFT', 'text', isActive ? 1 : 0.78);
|
||
}
|
||
|
||
const plan = frame(sb, 22, 840, NAV - 44, 150, C.surface, 0.10, 24, 'Plan card');
|
||
plan.effects = effects('glass');
|
||
addStroke(plan, C.surface, 0.14);
|
||
ellipse(plan, 155, -24, 110, 110, C.gold, 0.18, 'Plan glow');
|
||
await txt(plan, 18, 18, 140, 20, 'Luxe Suite Plan', 13, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(plan, 18, 43, 176, 36, 'Enterprise dashboard suite for multi-location beauty centers.', 10, C.rose2, 'regular', 'LEFT', 'text', 0.86, 'TOP');
|
||
const small = frame(plan, 18, 92, 118, 34, C.surface, 0.12, 14, 'Upgrade chip');
|
||
addStroke(small, C.gold, 0.22);
|
||
await txt(small, 0, 0, 118, 34, 'Premium active', 11, C.gold2, 'bold', 'CENTER');
|
||
}
|
||
|
||
async function topbar(root, title, subtitle, path) {
|
||
const top = frame(root, NAV, 0, W - NAV, TOP, C.surface, 0.58, 0, 'Glass top bar');
|
||
top.effects = [{ type: 'BACKGROUND_BLUR', radius: 18, visible: true }];
|
||
rect(top, 0, TOP - 1, W - NAV, 1, C.border, 0.78, 0, 'Top border');
|
||
|
||
await txt(top, 34, 22, 360, 28, title, 25, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(top, 34, 55, 560, 18, subtitle, 12, C.muted, 'medium', 'LEFT', 'text', 1);
|
||
await pill(top, 430, 32, 156, 32, path || '/dashboard', C.wine700, C.roseSoft);
|
||
|
||
const search = frame(top, 604, 24, 248, 48, C.surface, 0.72, 16, 'Search');
|
||
addStroke(search, C.border, 0.9);
|
||
iconSearch(search, 16, 14, C.muted);
|
||
await txt(search, 48, 0, 178, 48, 'Search clients, bookings, invoices', 12, C.muted, 'regular', 'LEFT', 'text', 0.84);
|
||
|
||
const date = frame(top, 866, 24, 116, 48, C.cream, 0.92, 16, 'Date filter');
|
||
addStroke(date, C.borderWine, 0.6);
|
||
await txt(date, 14, 0, 88, 48, 'This month', 12, C.wine700, 'bold', 'CENTER');
|
||
|
||
const action = frame(top, 996, 24, 96, 48, C.wine700, 1, 16, 'Primary action opens /dashboard/appointments/new');
|
||
action.fills = gradient([[0, C.wine700, 1], [0.58, C.wine500, 1], [1, C.rose, 1]], 'horizontal');
|
||
action.effects = effects('soft');
|
||
await txt(action, 0, 0, 96, 48, 'New', 12, C.surface, 'bold', 'CENTER');
|
||
|
||
const bell = frame(top, 1106, 24, 42, 48, C.surface, 0.82, 16, 'Notification bell opens /dashboard/notifications');
|
||
addStroke(bell, C.borderWine, 0.7);
|
||
iconChip(bell, 7, 10, 'notifications', C.wine700, C.roseSoft, 0.8, 28);
|
||
ellipse(bell, 28, 12, 8, 8, C.danger, 1, 'Unread notification dot');
|
||
}
|
||
|
||
function iconSearch(parent, x, y, color) {
|
||
ellipse(parent, x, y, 14, 14, color, 0.8, 'Search circle').strokes = [];
|
||
ellipse(parent, x + 3, y + 3, 8, 8, C.surface, 1, 'Search cutout');
|
||
line(parent, x + 12, y + 12, x + 20, y + 20, color, 0.8, 2, 'Search handle');
|
||
}
|
||
|
||
async function avatar(parent, x, y, size, initials, bg, color) {
|
||
const a = frame(parent, x, y, size, size, bg, 0.22, size / 2, 'Avatar ' + initials);
|
||
addStroke(a, bg, 0.55);
|
||
a.fills = gradient([[0, bg, 0.92], [1, C.surface, 0.55]], 'diagonal');
|
||
await txt(a, 0, 0, size, size, initials, Math.max(10, size * 0.32), color, 'bold', 'CENTER', 'display');
|
||
return a;
|
||
}
|
||
|
||
function iconChip(parent, x, y, kind, color = C.wine700, bg = C.wine700, bgOpacity = 0.1, size = 34) {
|
||
const chip = frame(parent, x, y, size, size, bg, bgOpacity, Math.floor(size / 2.8), 'Icon ' + kind);
|
||
chip.clipsContent = true;
|
||
const s = size;
|
||
const cx = s / 2;
|
||
const cy = s / 2;
|
||
if (kind === 'calendar') {
|
||
const box = rect(chip, 9, 10, s - 18, s - 16, color, 0.15, 4, 'Calendar box');
|
||
addStroke(box, color, 0.85, 1.4);
|
||
rect(chip, 11, 14, s - 22, 2, color, 0.8, 1, 'Calendar top');
|
||
rect(chip, 14, 20, 4, 4, color, 0.9, 1, 'Date dot');
|
||
rect(chip, 21, 20, 4, 4, color, 0.45, 1, 'Date dot');
|
||
} else if (kind === 'clients') {
|
||
ellipse(chip, 10, 9, 9, 9, color, 0.92, 'Client head');
|
||
ellipse(chip, 18, 11, 8, 8, color, 0.48, 'Client head');
|
||
rect(chip, 8, 21, 14, 6, color, 0.82, 4, 'Client body');
|
||
rect(chip, 18, 22, 10, 5, color, 0.35, 4, 'Client body');
|
||
} else if (kind === 'services') {
|
||
ellipse(chip, 10, 10, 7, 7, color, 0.85, 'Gem');
|
||
ellipse(chip, 20, 10, 7, 7, color, 0.55, 'Gem');
|
||
ellipse(chip, 15, 19, 7, 7, color, 0.7, 'Gem');
|
||
} else if (kind === 'staff') {
|
||
rect(chip, 9, 10, s - 18, 4, color, 0.85, 2, 'Staff bar');
|
||
rect(chip, 9, 17, s - 22, 4, color, 0.55, 2, 'Staff bar');
|
||
rect(chip, 9, 24, s - 15, 4, color, 0.35, 2, 'Staff bar');
|
||
} else if (kind === 'inventory') {
|
||
const box = rect(chip, 10, 12, s - 20, s - 18, color, 0.2, 5, 'Box');
|
||
addStroke(box, color, 0.8, 1.4);
|
||
line(chip, 10, 18, s - 10, 18, color, 0.5, 1.4, 'Box line');
|
||
} else if (kind === 'payments') {
|
||
const card = rect(chip, 8, 12, s - 16, s - 20, color, 0.16, 5, 'Payment card');
|
||
addStroke(card, color, 0.9, 1.4);
|
||
rect(chip, 11, 17, s - 22, 3, color, 0.75, 1, 'Card strip');
|
||
} else if (kind === 'marketing') {
|
||
ellipse(chip, 9, 10, 16, 16, color, 0.18, 'Megaphone circle');
|
||
line(chip, 12, 20, 25, 13, color, 0.9, 3, 'Megaphone');
|
||
rect(chip, 18, 21, 4, 7, color, 0.7, 2, 'Handle');
|
||
} else if (kind === 'reviews') {
|
||
ellipse(chip, 9, 9, 16, 16, color, 0.16, 'Review bubble');
|
||
rect(chip, 13, 14, 11, 3, color, 0.8, 2, 'Review line');
|
||
rect(chip, 13, 20, 7, 3, color, 0.45, 2, 'Review line');
|
||
} else if (kind === 'analytics' || kind === 'overview') {
|
||
rect(chip, 9, 22, 4, 6, color, 0.55, 2, 'Bar');
|
||
rect(chip, 16, 16, 4, 12, color, 0.75, 2, 'Bar');
|
||
rect(chip, 23, 10, 4, 18, color, 0.95, 2, 'Bar');
|
||
} else if (kind === 'orders') {
|
||
const doc = rect(chip, 10, 8, s - 20, s - 14, color, 0.15, 5, 'Order sheet');
|
||
addStroke(doc, color, 0.8, 1.2);
|
||
rect(chip, 14, 14, s - 28, 3, color, 0.72, 2, 'Order line');
|
||
rect(chip, 14, 21, s - 34, 3, color, 0.46, 2, 'Order line');
|
||
} else if (kind === 'store') {
|
||
rect(chip, 9, 14, s - 18, s - 20, color, 0.18, 5, 'Store shelf');
|
||
addStroke(chip.children[0], color, 0.75, 1.2);
|
||
rect(chip, 12, 9, s - 24, 8, color, 0.72, 4, 'Awning');
|
||
rect(chip, 15, 21, 5, 7, color, 0.8, 2, 'Product');
|
||
rect(chip, 23, 20, 5, 8, color, 0.45, 2, 'Product');
|
||
} else if (kind === 'notifications') {
|
||
ellipse(chip, cx - 8, cy - 4, 16, 14, color, 0.16, 'Bell body glow');
|
||
line(chip, cx - 7, cy + 4, cx + 7, cy + 4, color, 0.9, 2.2, 'Bell base');
|
||
line(chip, cx - 5, cy + 4, cx - 4, cy - 5, color, 0.86, 2, 'Bell left');
|
||
line(chip, cx + 5, cy + 4, cx + 4, cy - 5, color, 0.86, 2, 'Bell right');
|
||
ellipse(chip, cx - 2, cy + 7, 4, 4, color, 0.78, 'Bell clapper');
|
||
} else if (kind === 'messages') {
|
||
const bubble = rect(chip, 8, 10, s - 16, s - 19, color, 0.16, 7, 'Message bubble');
|
||
addStroke(bubble, color, 0.82, 1.2);
|
||
rect(chip, 13, 16, s - 28, 3, color, 0.74, 2, 'Message line');
|
||
rect(chip, 13, 22, s - 34, 3, color, 0.45, 2, 'Message line');
|
||
} else if (kind === 'profile') {
|
||
ellipse(chip, cx - 5, cy - 8, 10, 10, color, 0.86, 'Profile head');
|
||
rect(chip, cx - 10, cy + 5, 20, 8, color, 0.22, 6, 'Profile body');
|
||
addStroke(chip.children[1], color, 0.7, 1.2);
|
||
} else if (kind === 'branches') {
|
||
ellipse(chip, cx - 8, cy - 8, 16, 16, color, 0.17, 'Pin base');
|
||
ellipse(chip, cx - 4, cy - 7, 8, 8, color, 0.9, 'Pin dot');
|
||
line(chip, cx, cy, cx, cy + 11, color, 0.8, 2, 'Pin stem');
|
||
} else if (kind === 'settings') {
|
||
ellipse(chip, cx - 8, cy - 8, 16, 16, color, 0.15, 'Gear');
|
||
addStroke(chip.children[0], color, 0.85, 1.4);
|
||
ellipse(chip, cx - 3, cy - 3, 6, 6, color, 0.8, 'Gear center');
|
||
} else {
|
||
ellipse(chip, cx - 6, cy - 6, 12, 12, color, 0.85, 'Icon dot');
|
||
}
|
||
return chip;
|
||
}
|
||
|
||
function card(parent, x, y, w, h, radius = 24, name = 'Card') {
|
||
const node = frame(parent, x, y, w, h, C.surface, 0.86, radius, name);
|
||
node.effects = effects('card');
|
||
addStroke(node, C.border, 0.72);
|
||
return node;
|
||
}
|
||
|
||
function glass(parent, x, y, w, h, radius = 24, name = 'Glass card') {
|
||
const node = frame(parent, x, y, w, h, C.surface, 0.52, radius, name);
|
||
node.effects = effects('glass');
|
||
addStroke(node, C.surface, 0.52);
|
||
return node;
|
||
}
|
||
|
||
function luxuryHero(parent, x, y, w, h, name = 'Luxury hero') {
|
||
const node = frame(parent, x, y, w, h, C.wine900, 1, 30, name);
|
||
node.fills = gradient([[0, C.ink, 1], [0.42, C.wine800, 1], [0.78, C.wine500, 1], [1, C.gold, 0.92]], 'diagonal');
|
||
node.effects = effects('hero');
|
||
ellipse(node, w - 170, -110, 260, 260, C.gold, 0.22, 'Hero glow gold');
|
||
ellipse(node, -120, h - 120, 260, 220, C.rose, 0.16, 'Hero glow rose');
|
||
addStroke(node, C.surface, 0.10);
|
||
return node;
|
||
}
|
||
|
||
async function metricCard(parent, x, y, w, h, label, value, trend, kind, accent = C.wine700) {
|
||
const c = card(parent, x, y, w, h, 22, 'Metric ' + label);
|
||
rect(c, 0, 0, w, 4, accent, 1, 0, 'Metric accent');
|
||
iconChip(c, w - 54, 20, kind, accent, accent, 0.12, 36);
|
||
await txt(c, 18, 18, w - 86, 34, value, 27, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(c, 18, 55, w - 40, 18, label, 12, C.muted, 'medium', 'LEFT');
|
||
await pill(c, 18, h - 36, 112, 24, trend, trend.includes('-') ? C.danger : C.success, trend.includes('-') ? C.dangerSoft : C.successSoft);
|
||
return c;
|
||
}
|
||
|
||
async function pill(parent, x, y, w, h, label, color = C.wine700, bg = C.roseSoft) {
|
||
const p = frame(parent, x, y, w, h, bg, 1, h / 2, 'Pill ' + label);
|
||
addStroke(p, color, 0.12);
|
||
await txt(p, 0, 0, w, h, label, 10, color, 'bold', 'CENTER');
|
||
return p;
|
||
}
|
||
|
||
async function sectionHeader(parent, x, y, w, title, action) {
|
||
await txt(parent, x, y, w * 0.6, 24, title, 16, C.text, 'bold', 'LEFT', 'display');
|
||
if (action) await pill(parent, x + w - 118, y - 2, 118, 28, action, C.wine700, C.roseSoft);
|
||
}
|
||
|
||
function barChart(parent, x, y, w, h, values, color = C.wine700, soft = C.roseSoft) {
|
||
const max = Math.max.apply(null, values);
|
||
const gap = 10;
|
||
const bw = (w - gap * (values.length - 1)) / values.length;
|
||
for (let i = 0; i < values.length; i++) {
|
||
const bh = Math.max(10, values[i] / max * h);
|
||
rect(parent, x + i * (bw + gap), y + h - bh, bw, bh, color, 0.35 + (i / values.length) * 0.55, Math.min(8, bw / 2), 'Chart bar');
|
||
rect(parent, x + i * (bw + gap), y + h - bh, bw, 5, C.surface, 0.14, Math.min(5, bw / 2), 'Bar highlight');
|
||
}
|
||
rect(parent, x, y + h + 12, w, 1, soft, 1, 0, 'Chart base');
|
||
}
|
||
|
||
function lineChart(parent, x, y, w, h, points, color = C.wine700) {
|
||
const max = Math.max.apply(null, points);
|
||
const min = Math.min.apply(null, points);
|
||
const coords = points.map((p, i) => {
|
||
const px = x + i * (w / (points.length - 1));
|
||
const py = y + h - ((p - min) / Math.max(max - min, 1)) * h;
|
||
return [px, py];
|
||
});
|
||
for (let i = 0; i < coords.length - 1; i++) line(parent, coords[i][0], coords[i][1], coords[i + 1][0], coords[i + 1][1], color, 0.86, 3, 'Trend line');
|
||
coords.forEach(([px, py]) => {
|
||
ellipse(parent, px - 5, py - 5, 10, 10, C.surface, 1, 'Line dot shell');
|
||
ellipse(parent, px - 3, py - 3, 6, 6, color, 1, 'Line dot');
|
||
});
|
||
}
|
||
|
||
function spark(parent, x, y, w, h, color, points) {
|
||
lineChart(parent, x, y, w, h, points, color);
|
||
}
|
||
|
||
async function table(parent, x, y, w, headers, rows, widths, rowH = 48) {
|
||
const head = frame(parent, x, y, w, 42, C.cream, 0.86, 16, 'Table header');
|
||
addStroke(head, C.border, 0.55);
|
||
let cx = 16;
|
||
for (let i = 0; i < headers.length; i++) {
|
||
await txt(head, cx, 0, widths[i] - 16, 42, headers[i], 10, C.muted, 'bold', 'LEFT');
|
||
cx += widths[i];
|
||
}
|
||
for (let r = 0; r < rows.length; r++) {
|
||
const row = frame(parent, x, y + 48 + r * rowH, w, rowH, r % 2 ? C.surface2 : C.surface, r % 2 ? 0.55 : 0.28, 0, 'Table row');
|
||
rect(row, 16, rowH - 1, w - 32, 1, C.border, 0.55, 0, 'Row divider');
|
||
cx = 16;
|
||
for (let i = 0; i < rows[r].length; i++) {
|
||
await txt(row, cx, 0, widths[i] - 16, rowH, rows[r][i], 11, i === 0 ? C.text : C.text2, i === 0 ? 'semibold' : 'medium', 'LEFT');
|
||
cx += widths[i];
|
||
}
|
||
}
|
||
}
|
||
|
||
async function labelValue(parent, x, y, label, value, color = C.text) {
|
||
await txt(parent, x, y, 150, 16, label, 10, C.muted, 'bold', 'LEFT');
|
||
await txt(parent, x, y + 18, 190, 22, value, 14, color, 'bold', 'LEFT', 'display');
|
||
}
|
||
|
||
async function progressBar(parent, x, y, w, label, pct, color = C.wine700) {
|
||
await txt(parent, x, y, w - 44, 18, label, 11, C.text2, 'semibold', 'LEFT');
|
||
await txt(parent, x + w - 42, y, 42, 18, pct + '%', 10, C.muted, 'bold', 'RIGHT');
|
||
rect(parent, x, y + 26, w, 8, C.cream2, 1, 4, 'Progress track');
|
||
rect(parent, x, y + 26, Math.max(8, w * pct / 100), 8, color, 1, 4, 'Progress fill');
|
||
}
|
||
|
||
async function formField(parent, x, y, w, label, value, wide = false) {
|
||
await txt(parent, x, y, w, 16, label, 10, C.muted, 'bold', 'LEFT');
|
||
const f = frame(parent, x, y + 22, w, wide ? 76 : 44, C.cream, 0.55, 14, 'Field ' + label);
|
||
addStroke(f, C.border, 0.9);
|
||
await txt(f, 14, 0, w - 28, wide ? 76 : 44, value, 12, C.text2, 'medium', 'LEFT', 'text', 1, wide ? 'TOP' : 'CENTER');
|
||
}
|
||
|
||
|
||
async function baseAuthFrame(index, title, subtitle, path) {
|
||
const root = figma.createFrame();
|
||
figma.currentPage.appendChild(root);
|
||
root.name = String(index).padStart(2, '0') + ' · ' + title;
|
||
const pos = screenGridPosition(index);
|
||
root.x = pos.x;
|
||
root.y = pos.y;
|
||
root.resize(W, H);
|
||
root.clipsContent = true;
|
||
root.fills = gradient([
|
||
[0, C.cream, 1],
|
||
[0.44, C.surface2, 1],
|
||
[1, C.champagne, 1]
|
||
], 'diagonal');
|
||
|
||
addAmbient(root);
|
||
ellipse(root, -180, 170, 520, 520, C.wine600, 0.08, 'Auth wine bloom');
|
||
ellipse(root, W - 260, 120, 420, 420, C.gold, 0.16, 'Auth gold bloom');
|
||
ellipse(root, W - 600, H - 220, 480, 300, C.rose, 0.12, 'Auth rose bloom');
|
||
|
||
const nav = glass(root, 48, 30, W - 96, 72, 26, 'Auth top navigation');
|
||
const mark = frame(nav, 22, 14, 44, 44, C.wine700, 1, 15, 'BeautyHub auth mark');
|
||
mark.fills = gradient([[0, C.wine700, 1], [0.55, C.rose, 1], [1, C.gold, 1]], 'diagonal');
|
||
mark.effects = effects('soft');
|
||
await txt(mark, 0, 0, 44, 44, 'BH', 15, C.surface, 'heavy', 'CENTER', 'display');
|
||
await txt(nav, 80, 16, 190, 22, 'BeautyHub SalonOS', 16, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(nav, 80, 42, 300, 14, 'Premium web access for beauty centers', 10, C.muted, 'medium', 'LEFT');
|
||
await pill(nav, 780, 20, 132, 32, path || '/auth', C.wine700, C.surface);
|
||
await pill(nav, 936, 20, 122, 32, 'Web only', C.wine700, C.roseSoft);
|
||
await pill(nav, 1072, 20, 146, 32, 'Luxury suite', C.goldDark, C.warningSoft);
|
||
await buttonComponent(nav, 1234, 16, 72, 40, 'Help', 'secondary', 'default', 'support');
|
||
|
||
await txt(root, 64, 928, 430, 20, 'Designed for premium salons, spas, clinics, and beauty center operations.', 11, C.muted, 'medium', 'LEFT');
|
||
return root;
|
||
}
|
||
|
||
async function buttonComponent(parent, x, y, w, h, label, variant = 'primary', state = 'default', icon = null) {
|
||
const disabled = state === 'disabled';
|
||
const loading = state === 'loading';
|
||
const pressed = state === 'pressed';
|
||
const hover = state === 'hover';
|
||
const node = frame(parent, x, y + (pressed ? 1 : 0), w, h, C.surface, 1, Math.min(16, h / 2), 'Button / ' + variant + ' / ' + state + ' / ' + label);
|
||
|
||
if (variant === 'primary') {
|
||
node.fills = gradient([[0, C.wine800, 1], [0.55, C.wine600, 1], [1, C.rose, 1]], 'horizontal');
|
||
node.effects = hover ? effects('hero') : effects('soft');
|
||
addStroke(node, C.surface, hover ? 0.28 : 0.14);
|
||
} else if (variant === 'gold') {
|
||
node.fills = gradient([[0, C.gold2, 1], [1, C.gold, 1]], 'horizontal');
|
||
node.effects = effects('soft');
|
||
addStroke(node, C.goldDark, 0.16);
|
||
} else if (variant === 'danger') {
|
||
node.fills = solid(disabled ? C.dangerSoft : C.danger, disabled ? 0.55 : 1);
|
||
addStroke(node, C.danger, 0.18);
|
||
} else if (variant === 'ghost') {
|
||
node.fills = solid(C.surface, 0.16);
|
||
addStroke(node, C.borderWine, 0.55);
|
||
} else if (variant === 'icon') {
|
||
node.fills = solid(C.cream, 0.78);
|
||
addStroke(node, C.border, 0.78);
|
||
} else {
|
||
node.fills = solid(C.surface, 0.82);
|
||
node.effects = effects('soft');
|
||
addStroke(node, hover ? C.wine500 : C.borderWine, hover ? 0.42 : 0.72);
|
||
}
|
||
|
||
if (disabled) node.opacity = 0.48;
|
||
if (loading) {
|
||
ellipse(node, 18, h / 2 - 4, 8, 8, variant === 'primary' ? C.surface : C.wine700, 0.95, 'Loading dot 1');
|
||
ellipse(node, 31, h / 2 - 4, 8, 8, variant === 'primary' ? C.surface : C.wine700, 0.62, 'Loading dot 2');
|
||
ellipse(node, 44, h / 2 - 4, 8, 8, variant === 'primary' ? C.surface : C.wine700, 0.32, 'Loading dot 3');
|
||
}
|
||
|
||
if (icon && !loading) {
|
||
const ix = 16;
|
||
const iy = h / 2 - 7;
|
||
if (icon === 'plus') {
|
||
line(node, ix, iy + 7, ix + 14, iy + 7, variant === 'primary' ? C.surface : C.wine700, 0.95, 2, 'Plus horizontal');
|
||
line(node, ix + 7, iy, ix + 7, iy + 14, variant === 'primary' ? C.surface : C.wine700, 0.95, 2, 'Plus vertical');
|
||
} else if (icon === 'lock') {
|
||
rect(node, ix, iy + 6, 16, 12, variant === 'primary' ? C.surface : C.wine700, 0.9, 4, 'Lock body');
|
||
ellipse(node, ix + 3, iy, 10, 11, variant === 'primary' ? C.surface : C.wine700, 0.32, 'Lock loop');
|
||
} else {
|
||
ellipse(node, ix, iy, 14, 14, variant === 'primary' ? C.surface : C.wine700, 0.92, 'Button icon dot');
|
||
rect(node, ix + 16, iy + 6, 10, 2, variant === 'primary' ? C.surface : C.wine700, 0.7, 2, 'Button icon line');
|
||
}
|
||
}
|
||
|
||
const textColor = variant === 'primary' || variant === 'danger' ? C.surface : (variant === 'gold' ? C.wine900 : C.wine700);
|
||
const leftPad = icon || loading ? 30 : 0;
|
||
await txt(node, leftPad, 0, w - leftPad, h, loading ? label : label, 12, textColor, 'bold', 'CENTER', 'text', disabled ? 0.7 : 1);
|
||
return node;
|
||
}
|
||
|
||
async function photoTile(parent, x, y, w, h, title, caption, accent = C.gold) {
|
||
const tile = frame(parent, x, y, w, h, C.ink, 1, 26, 'Image tile - ' + title);
|
||
tile.fills = gradient([[0, C.ink, 1], [0.44, C.wine800, 1], [1, accent, 0.72]], 'diagonal');
|
||
tile.effects = effects('soft');
|
||
tile.clipsContent = true;
|
||
ellipse(tile, w - 120, -70, 210, 180, C.surface, 0.12, 'Photo light bloom');
|
||
ellipse(tile, -54, h - 72, 190, 150, C.rose, 0.16, 'Photo rose bloom');
|
||
rect(tile, 0, h - 76, w, 76, C.ink, 0.24, 0, 'Photo caption veil');
|
||
rect(tile, 24, 30, Math.max(90, w * 0.38), Math.max(58, h * 0.28), C.surface, 0.12, 22, 'Abstract salon window');
|
||
addStroke(tile.children[tile.children.length - 1], C.surface, 0.2);
|
||
rect(tile, w - 126, Math.max(34, h * 0.22), 76, Math.max(88, h * 0.36), C.surface, 0.1, 28, 'Abstract treatment chair');
|
||
ellipse(tile, w - 100, Math.max(48, h * 0.22) - 18, 42, 42, C.gold2, 0.28, 'Salon light');
|
||
await txt(tile, 24, h - 62, w - 48, 24, title, 16, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(tile, 24, h - 34, w - 48, 18, caption, 11, C.rose2, 'medium', 'LEFT');
|
||
return tile;
|
||
}
|
||
|
||
async function featureRow(parent, x, y, icon, title, body, color = C.wine700) {
|
||
const row = glass(parent, x, y, 286, 86, 22, 'Feature row - ' + title);
|
||
iconChip(row, 18, 22, icon, color, color, 0.12, 40);
|
||
await txt(row, 72, 16, 180, 20, title, 14, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(row, 72, 42, 184, 30, body, 10, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
return row;
|
||
}
|
||
|
||
async function simpleTimeline(parent, x, y, items, accent = C.wine700) {
|
||
for (let i = 0; i < items.length; i++) {
|
||
const yy = y + i * 68;
|
||
ellipse(parent, x, yy + 10, 16, 16, i === 0 ? accent : C.cream2, 1, 'Timeline dot');
|
||
addStroke(parent.children[parent.children.length - 1], accent, i === 0 ? 0 : 0.45, 1.2);
|
||
if (i < items.length - 1) line(parent, x + 8, yy + 30, x + 8, yy + 62, accent, 0.18, 2, 'Timeline connector');
|
||
await txt(parent, x + 28, yy, 220, 18, items[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(parent, x + 28, yy + 22, 260, 28, items[i][1], 10, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
}
|
||
}
|
||
|
||
async function modalShell(root, title, subtitle, w = 700, h = 650) {
|
||
const overlay = frame(root, NAV, TOP, W - NAV, H - TOP, C.ink, 0.34, 0, 'Modal backdrop overlay');
|
||
overlay.effects = [{ type: 'BACKGROUND_BLUR', radius: 8, visible: true }];
|
||
const mx = NAV + Math.round((W - NAV - w) / 2);
|
||
const my = TOP + Math.round((H - TOP - h) / 2);
|
||
const modal = card(root, mx, my, w, h, 34, title + ' modal');
|
||
modal.fills = solid(C.surface, 0.96);
|
||
await txt(modal, 34, 28, w - 156, 30, title, 25, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(modal, 34, 64, w - 156, 20, subtitle, 12, C.muted, 'medium', 'LEFT');
|
||
const close = frame(modal, w - 76, 28, 42, 42, C.cream, 0.86, 15, 'Close modal');
|
||
addStroke(close, C.border, 0.8);
|
||
line(close, 15, 15, 27, 27, C.wine700, 0.8, 2, 'Close slash');
|
||
line(close, 27, 15, 15, 27, C.wine700, 0.8, 2, 'Close slash');
|
||
return modal;
|
||
}
|
||
|
||
async function pageOnboarding(root) {
|
||
const hero = luxuryHero(root, 64, 132, 562, 790, 'Onboarding educational hero');
|
||
await txt(hero, 42, 38, 180, 18, 'PRODUCT TOUR', 11, C.gold2, 'bold', 'LEFT');
|
||
await txt(hero, 42, 82, 390, 126, 'Meet the operating system for luxury beauty centers.', 52, C.surface, 'heavy', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(hero, 42, 230, 398, 64, 'BeautyHub brings bookings, clients, staff, inventory, marketing, payments, and reports into one elegant web dashboard.', 15, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await buttonComponent(hero, 42, 326, 184, 48, 'Start tour', 'gold', 'default', 'plus');
|
||
await buttonComponent(hero, 244, 326, 164, 48, 'Explore suite', 'ghost', 'default');
|
||
|
||
await photoTile(hero, 42, 426, 228, 224, 'Salon operations', 'Calendar, rooms, and specialists', C.gold);
|
||
await photoTile(hero, 294, 426, 214, 224, 'Client experience', 'VIP profiles and preferences', C.rose);
|
||
const quote = glass(hero, 42, 684, 466, 70, 24, 'Onboarding promise');
|
||
await txt(quote, 24, 16, 418, 18, 'A premium web-only admin suite for real salon management.', 14, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(quote, 24, 42, 390, 16, 'No mobile screens. Every action state becomes a designed web interface.', 10, C.rose2, 'medium', 'LEFT');
|
||
|
||
const guide = card(root, 666, 132, 706, 790, 34, 'Onboarding system introduction');
|
||
await sectionHeader(guide, 32, 30, 642, 'What the system includes', 'Web suite');
|
||
await txt(guide, 32, 72, 480, 38, 'Use this introduction to explain the product before the user reaches login, register, and the full dashboard suite.', 13, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
|
||
await featureRow(guide, 32, 138, 'calendar', 'Bookings engine', 'Real-time calendar, appointment details, and modal creation states.', C.wine700);
|
||
await featureRow(guide, 350, 138, 'clients', 'Client intelligence', 'Profiles, medical records, notes, loyalty, and consent history.', C.rose);
|
||
await featureRow(guide, 32, 248, 'staff', 'Team operations', 'Schedules, specialist profiles, commissions, rooms, and capacity.', C.goldDark);
|
||
await featureRow(guide, 350, 248, 'inventory', 'Retail control', 'Inventory, products, orders, storefront analytics, and stock alerts.', C.violet);
|
||
await featureRow(guide, 32, 358, 'marketing', 'Growth studio', 'Campaigns, rewards, reviews, messages, and notifications.', C.wine500);
|
||
await featureRow(guide, 350, 358, 'payments', 'Revenue suite', 'Payments, subscription plans, reports, and revenue analytics.', C.success);
|
||
|
||
const images = frame(guide, 32, 498, 642, 172, C.cream, 0.48, 30, 'Image gallery strip');
|
||
addStroke(images, C.border, 0.7);
|
||
await photoTile(images, 20, 20, 188, 132, 'Reception desk', 'Premium check-in', C.gold);
|
||
await photoTile(images, 228, 20, 188, 132, 'Treatment suite', 'Services and care', C.rose);
|
||
await photoTile(images, 436, 20, 186, 132, 'Retail shelf', 'Products and orders', C.violet);
|
||
|
||
const steps = glass(guide, 32, 702, 642, 58, 22, 'Onboarding route strip');
|
||
await pill(steps, 18, 14, 122, 30, '/onboarding', C.wine700, C.roseSoft);
|
||
await pill(steps, 154, 14, 96, 30, '/login', C.wine700, C.cream);
|
||
await pill(steps, 264, 14, 112, 30, '/register', C.goldDark, C.warningSoft);
|
||
await pill(steps, 390, 14, 180, 30, '30 dashboard interfaces', C.success, C.successSoft);
|
||
}
|
||
|
||
// Web Auth 02
|
||
async function pageLogin(root) {
|
||
const panel = card(root, 84, 148, 520, 728, 36, 'Login form panel');
|
||
await txt(panel, 42, 42, 330, 34, 'Welcome back', 34, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(panel, 42, 86, 356, 38, 'Sign in to manage bookings, clients, payments, staff, and growth for your luxury salon.', 13, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await formField(panel, 42, 158, 436, 'Email address', 'owner@luxebeauty.com');
|
||
await formField(panel, 42, 238, 436, 'Password', '••••••••••••');
|
||
const row = frame(panel, 42, 320, 436, 34, C.surface, 0, 0, 'Login options');
|
||
rect(row, 0, 8, 18, 18, C.wine700, 1, 6, 'Remember check');
|
||
await txt(row, 28, 0, 140, 34, 'Remember this device', 11, C.text2, 'semibold', 'LEFT');
|
||
await txt(row, 290, 0, 146, 34, 'Forgot password?', 11, C.wine700, 'bold', 'RIGHT');
|
||
await buttonComponent(panel, 42, 384, 436, 50, 'Sign in securely', 'primary', 'default', 'lock');
|
||
await buttonComponent(panel, 42, 454, 206, 46, 'Google SSO', 'secondary', 'default');
|
||
await buttonComponent(panel, 272, 454, 206, 46, 'Apple SSO', 'secondary', 'default');
|
||
const trust = glass(panel, 42, 556, 436, 112, 24, 'Security trust card');
|
||
iconChip(trust, 22, 24, 'settings', C.success, C.success, 0.12, 42);
|
||
await txt(trust, 78, 22, 170, 22, 'Enterprise security', 15, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(trust, 78, 50, 300, 36, 'Role-based permissions, secure checkout access, and encrypted staff sessions.', 11, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
|
||
const preview = luxuryHero(root, 660, 148, 696, 728, 'Login dashboard preview');
|
||
await txt(preview, 42, 38, 180, 18, 'TODAY AT A GLANCE', 11, C.gold2, 'bold', 'LEFT');
|
||
await txt(preview, 42, 78, 440, 60, 'Your salon is already glowing today.', 42, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(preview, 42, 152, 400, 42, 'Secure access opens directly into your executive dashboard, live calendar, and checkout tools.', 13, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await metricCard(preview, 42, 244, 180, 122, 'Bookings today', '42', '+18%', 'calendar', C.gold2);
|
||
await metricCard(preview, 244, 244, 180, 122, 'Revenue booked', '$18.4k', '+12%', 'payments', C.rose2);
|
||
await metricCard(preview, 446, 244, 180, 122, 'VIP guests', '11', '+7%', 'clients', C.gold2);
|
||
const activity = glass(preview, 42, 420, 584, 230, 26, 'Login activity preview');
|
||
await sectionHeader(activity, 24, 20, 536, 'Live secure activity', 'View logs');
|
||
const acts = [['Emma signed in', 'Owner · New York · 2 min ago'], ['Maya approved checkout', 'Manager · Downtown · 8 min ago'], ['Nora updated services', 'Stylist lead · 18 min ago']];
|
||
for (let i = 0; i < acts.length; i++) {
|
||
avatar(activity, 24, 72 + i * 48, 30, acts[i][0].slice(0, 2), i === 0 ? C.gold : C.wine700, C.surface);
|
||
await txt(activity, 68, 66 + i * 48, 190, 18, acts[i][0], 12, C.surface, 'bold', 'LEFT');
|
||
await txt(activity, 68, 86 + i * 48, 240, 16, acts[i][1], 10, C.rose2, 'medium', 'LEFT');
|
||
}
|
||
}
|
||
|
||
// Web Auth 03
|
||
async function pageRegister(root) {
|
||
const left = luxuryHero(root, 64, 132, 472, 790, 'Register value panel');
|
||
await txt(left, 40, 42, 160, 18, 'CREATE ACCOUNT', 11, C.gold2, 'bold', 'LEFT');
|
||
await txt(left, 40, 84, 330, 112, 'Build your premium salon command center.', 48, C.surface, 'heavy', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(left, 40, 224, 330, 58, 'Register a new beauty center, choose your launch plan, and invite your first operators.', 14, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
const plan = glass(left, 40, 340, 392, 282, 28, 'Selected plan');
|
||
await txt(plan, 24, 22, 140, 18, 'SELECTED PLAN', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(plan, 24, 58, 190, 38, 'Luxe Growth', 34, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(plan, 24, 112, 250, 36, '$249/month · Includes 3 branches and 25 team seats.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await progressBar(plan, 24, 182, 326, 'Launch capacity', 84, C.gold2);
|
||
await buttonComponent(plan, 24, 226, 166, 40, 'Choose plan', 'gold', 'default');
|
||
await buttonComponent(plan, 206, 226, 146, 40, 'Book demo', 'ghost', 'default');
|
||
|
||
const form = card(root, 584, 132, 788, 790, 36, 'Registration form');
|
||
await sectionHeader(form, 34, 30, 720, 'Create your salon account', 'Step 1 of 3');
|
||
await txt(form, 34, 72, 420, 20, 'Business profile', 13, C.wine700, 'bold', 'LEFT');
|
||
await formField(form, 34, 110, 338, 'Salon or beauty center name', 'Luxe Beauty House');
|
||
await formField(form, 404, 110, 338, 'Business email', 'hello@luxebeauty.com');
|
||
await formField(form, 34, 190, 338, 'Owner full name', 'Emma Laurent');
|
||
await formField(form, 404, 190, 338, 'Primary location', 'New York, United States');
|
||
await txt(form, 34, 304, 420, 20, 'Business type', 13, C.wine700, 'bold', 'LEFT');
|
||
const types = [['Luxury Salon', C.wine700], ['Beauty Center', C.goldDark], ['Spa & Wellness', C.rose], ['Med Beauty', C.violet]];
|
||
for (let i = 0; i < types.length; i++) {
|
||
const t = frame(form, 34 + i * 178, 342, 154, 86, C.cream, 0.76, 22, 'Business type ' + types[i][0]);
|
||
addStroke(t, i === 0 ? C.wine700 : C.border, i === 0 ? 0.7 : 0.7, i === 0 ? 2 : 1);
|
||
iconChip(t, 18, 18, i === 0 ? 'services' : 'branches', types[i][1], types[i][1], 0.12, 34);
|
||
await txt(t, 18, 58, 110, 16, types[i][0], 11, C.text, 'bold', 'LEFT');
|
||
}
|
||
const compliance = glass(form, 34, 476, 708, 138, 28, 'Compliance checklist');
|
||
await txt(compliance, 24, 18, 200, 22, 'Launch agreement', 17, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(compliance, 24, 48, 420, 18, 'Confirm account ownership, team invitation permission, and booking policy setup.', 11, C.muted, 'medium', 'LEFT');
|
||
await pill(compliance, 24, 86, 150, 30, 'Owner verified', C.success, C.successSoft);
|
||
await pill(compliance, 188, 86, 152, 30, 'Policy pending', C.warning, C.warningSoft);
|
||
await pill(compliance, 354, 86, 142, 30, 'Team seats 12', C.wine700, C.roseSoft);
|
||
await buttonComponent(form, 34, 658, 214, 50, 'Create account', 'primary', 'default', 'plus');
|
||
await buttonComponent(form, 264, 658, 180, 50, 'Save business', 'secondary', 'default');
|
||
await buttonComponent(form, 460, 658, 164, 50, 'Book demo', 'gold', 'default');
|
||
}
|
||
|
||
// Page 04
|
||
async function pageOverview(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
|
||
const hero = luxuryHero(root, x, y, 606, 248, 'Overview hero');
|
||
await txt(hero, 30, 28, 290, 18, 'TODAY AT LUXE BEAUTY', 10, C.gold2, 'bold', 'LEFT', 'text', 0.92);
|
||
await txt(hero, 30, 56, 330, 56, '$42,860', 48, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(hero, 30, 116, 360, 22, 'Luxury service revenue is 18.4% above forecast.', 13, C.rose2, 'medium', 'LEFT');
|
||
await pill(hero, 30, 158, 128, 30, '+ $6.8k vs target', C.goldDark, C.gold2);
|
||
await pill(hero, 170, 158, 114, 30, '94 bookings', C.surface, C.surface);
|
||
barChart(hero, 380, 62, 180, 110, [42, 68, 54, 88, 74, 96, 86], C.gold, C.surface);
|
||
await txt(hero, 392, 190, 170, 20, 'Revenue by day', 11, C.rose2, 'medium', 'CENTER');
|
||
|
||
const live = glass(root, x + 630, y, 244, 248, 28, 'Live operations');
|
||
await sectionHeader(live, 20, 18, 204, 'Live now', 'View floor');
|
||
const liveItems = [
|
||
['Suite 01', 'Balayage with Olivia', '72 min'],
|
||
['Suite 03', 'Hydrafacial Elite', '38 min'],
|
||
['Chair 07', 'Gloss treatment', '14 min']
|
||
];
|
||
for (let i = 0; i < liveItems.length; i++) {
|
||
const iy = 60 + i * 54;
|
||
rect(live, 20, iy, 4, 34, i === 0 ? C.gold : i === 1 ? C.rose : C.wine600, 1, 3, 'Live status');
|
||
await txt(live, 34, iy - 3, 120, 17, liveItems[i][0], 11, C.muted, 'bold', 'LEFT');
|
||
await txt(live, 34, iy + 17, 150, 18, liveItems[i][1], 12, C.text, 'semibold', 'LEFT');
|
||
await pill(live, 160, iy + 9, 62, 24, liveItems[i][2], C.wine700, C.roseSoft);
|
||
}
|
||
|
||
const vip = card(root, x + 898, y, 198, 248, 28, 'VIP client card');
|
||
ellipse(vip, 34, 26, 78, 78, C.rose, 0.18, 'Portrait glow');
|
||
await avatar(vip, 48, 40, 52, 'AR', C.rose, C.wine700);
|
||
await txt(vip, 22, 110, 154, 24, 'Ariana Reed', 18, C.text, 'bold', 'CENTER', 'display');
|
||
await txt(vip, 22, 136, 154, 17, 'Diamond member', 11, C.muted, 'medium', 'CENTER');
|
||
await pill(vip, 42, 166, 114, 28, '$8.4k lifetime', C.goldDark, C.warningSoft);
|
||
await txt(vip, 28, 204, 142, 20, 'Next visit in 2 days', 11, C.text2, 'semibold', 'CENTER');
|
||
|
||
const mw = (CONTENT_W - 3 * 18) / 4;
|
||
await metricCard(root, x, y + 278, mw, 128, 'Booked revenue', '$128.4k', '+14.2%', 'payments', C.wine700);
|
||
await metricCard(root, x + (mw + 18), y + 278, mw, 128, 'Occupancy', '86%', '+8.7%', 'calendar', C.goldDark);
|
||
await metricCard(root, x + 2 * (mw + 18), y + 278, mw, 128, 'New clients', '342', '+21.0%', 'clients', C.success);
|
||
await metricCard(root, x + 3 * (mw + 18), y + 278, mw, 128, 'Retail attach', '34%', '-2.1%', 'inventory', C.rose);
|
||
|
||
const demand = card(root, x, y + 438, 406, 396, 28, 'Service demand');
|
||
await sectionHeader(demand, 22, 20, 362, 'Service demand', 'Optimize');
|
||
const services = [
|
||
['Color correction', 92, C.wine700], ['Signature facial', 78, C.rose], ['Keratin ritual', 66, C.goldDark], ['Lash lift', 54, C.violet], ['Bridal styling', 47, C.blue]
|
||
];
|
||
for (let i = 0; i < services.length; i++) await progressBar(demand, 24, 70 + i * 55, 350, services[i][0], services[i][1], services[i][2]);
|
||
|
||
const chart = card(root, x + 430, y + 438, 398, 396, 28, 'Revenue chart');
|
||
await sectionHeader(chart, 22, 20, 354, 'Monthly revenue curve', 'Export');
|
||
lineChart(chart, 34, 110, 326, 160, [24, 34, 31, 44, 51, 58, 72, 68, 79, 91, 86, 104], C.wine700);
|
||
await labelValue(chart, 30, 298, 'Projected close', '$186.2k', C.wine700);
|
||
await labelValue(chart, 190, 298, 'Gross margin', '71.8%', C.success);
|
||
|
||
const appointments = card(root, x + 852, y + 438, 244, 396, 28, 'Appointments timeline');
|
||
await sectionHeader(appointments, 20, 20, 204, 'Next arrivals', null);
|
||
const appts = [['10:30', 'Mia Chen', 'Luxury blowout'], ['11:15', 'Sofia King', 'Rose gold color'], ['12:40', 'Emma Fox', 'Diamond facial'], ['14:00', 'Nora Bell', 'Brow sculpt']];
|
||
for (let i = 0; i < appts.length; i++) {
|
||
const iy = 70 + i * 70;
|
||
rect(appointments, 26, iy + 8, 2, 42, C.borderWine, 1, 1, 'Timeline');
|
||
ellipse(appointments, 20, iy + 10, 14, 14, i === 0 ? C.gold : C.wine600, 1, 'Timeline dot');
|
||
await txt(appointments, 44, iy, 52, 18, appts[i][0], 11, C.goldDark, 'bold', 'LEFT');
|
||
await txt(appointments, 98, iy, 112, 18, appts[i][1], 12, C.text, 'bold', 'LEFT');
|
||
await txt(appointments, 44, iy + 24, 164, 18, appts[i][2], 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
}
|
||
|
||
// Page 05
|
||
async function pageCalendar(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const cal = card(root, x, y, 748, 720, 30, 'Week calendar');
|
||
await txt(cal, 24, 22, 260, 30, 'June 10–16, 2026', 22, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(cal, 24, 56, 380, 18, 'Resource-aware scheduling for suites, chairs, and specialists.', 12, C.muted, 'medium', 'LEFT');
|
||
await pill(cal, 560, 24, 74, 30, 'Day', C.muted, C.cream);
|
||
await pill(cal, 642, 24, 80, 30, 'Week', C.surface, C.wine700);
|
||
|
||
const days = ['Mon 10', 'Tue 11', 'Wed 12', 'Thu 13', 'Fri 14', 'Sat 15', 'Sun 16'];
|
||
const gridX = 86;
|
||
const gridY = 114;
|
||
const colW = 88;
|
||
const rowH = 62;
|
||
for (let h = 0; h < 8; h++) {
|
||
await txt(cal, 24, gridY + h * rowH - 6, 44, 18, (9 + h) + ':00', 10, C.faint, 'medium', 'LEFT');
|
||
rect(cal, gridX, gridY + h * rowH, 622, 1, C.border, 0.62, 0, 'Calendar grid line');
|
||
}
|
||
for (let i = 0; i < days.length; i++) {
|
||
await txt(cal, gridX + i * colW, 88, colW - 8, 20, days[i], 11, i === 4 ? C.wine700 : C.muted, 'bold', 'CENTER');
|
||
rect(cal, gridX + i * colW, gridY, 1, 496, C.border, 0.62, 0, 'Calendar vertical');
|
||
}
|
||
rect(cal, gridX + 4 * colW + 4, gridY - 8, colW - 10, 512, C.roseSoft, 0.46, 18, 'Highlighted day');
|
||
|
||
await bookingBlock(cal, gridX + 6, gridY + 26, colW - 14, 96, 'Color Suite', 'Lily', C.wine700);
|
||
await bookingBlock(cal, gridX + colW + 7, gridY + 110, colW - 14, 118, 'Hydrafacial', 'Ariana', C.goldDark);
|
||
await bookingBlock(cal, gridX + 2 * colW + 8, gridY + 70, colW - 14, 88, 'Brow Sculpt', 'Nora', C.violet);
|
||
await bookingBlock(cal, gridX + 3 * colW + 8, gridY + 238, colW - 14, 124, 'Keratin', 'Maya', C.success);
|
||
await bookingBlock(cal, gridX + 4 * colW + 8, gridY + 58, colW - 14, 158, 'Rose Gold', 'Sofia', C.wine600);
|
||
await bookingBlock(cal, gridX + 4 * colW + 8, gridY + 274, colW - 14, 104, 'Lash Lift', 'Emma', C.rose);
|
||
await bookingBlock(cal, gridX + 5 * colW + 8, gridY + 140, colW - 14, 142, 'Bridal Trial', 'Vera', C.blue);
|
||
await bookingBlock(cal, gridX + 6 * colW + 8, gridY + 80, colW - 14, 180, 'Spa Ritual', 'Team', C.goldDark);
|
||
|
||
const resources = glass(cal, 24, 636, 700, 60, 18, 'Resource utilization');
|
||
await txt(resources, 18, 0, 140, 60, 'Resource health', 12, C.text, 'bold', 'LEFT');
|
||
await progressBar(resources, 168, 12, 150, 'Color suites', 86, C.wine700);
|
||
await progressBar(resources, 340, 12, 150, 'Spa rooms', 74, C.goldDark);
|
||
await progressBar(resources, 512, 12, 150, 'Stylists', 91, C.success);
|
||
|
||
const side = frame(root, x + 772, y, 324, 720, C.surface, 0.74, 30, 'Booking side panel');
|
||
side.effects = effects('card'); addStroke(side, C.border, 0.72);
|
||
await sectionHeader(side, 24, 22, 276, 'Selected booking', 'Edit');
|
||
const photo = luxuryHero(side, 24, 72, 276, 154, 'Booking photo');
|
||
await txt(photo, 24, 24, 190, 34, 'Rose Gold Color Ritual', 24, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(photo, 24, 72, 210, 34, 'Suite 04 · Olivia Hart · 120 minutes', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await labelValue(side, 28, 254, 'Client', 'Sofia King', C.text);
|
||
await labelValue(side, 166, 254, 'Deposit', '$120 paid', C.success);
|
||
await txt(side, 28, 330, 250, 22, 'Booking checklist', 14, C.text, 'bold', 'LEFT', 'display');
|
||
const checklist = ['Formula saved in profile', 'Patch test completed', 'Retail recommendations ready', 'VIP lounge prepared'];
|
||
for (let i = 0; i < checklist.length; i++) {
|
||
ellipse(side, 30, 370 + i * 36, 18, 18, i < 2 ? C.success : C.gold, i < 2 ? 1 : 0.24, 'Check dot');
|
||
if (i < 2) await txt(side, 30, 368 + i * 36, 18, 18, '✓', 10, C.surface, 'bold', 'CENTER');
|
||
await txt(side, 58, 365 + i * 36, 222, 24, checklist[i], 12, C.text2, 'medium', 'LEFT');
|
||
}
|
||
await sectionHeader(side, 24, 530, 276, 'VIP waitlist', null);
|
||
const wait = [['Mia Parker', 'Any color cancellation'], ['Ivy Brooks', 'Facial after 3 PM'], ['Zoe Fields', 'Saturday styling']];
|
||
for (let i = 0; i < wait.length; i++) {
|
||
await avatar(side, 26, 574 + i * 44, 30, wait[i][0].split(' ').map(s => s[0]).join(''), C.rose, C.wine700);
|
||
await txt(side, 66, 570 + i * 44, 110, 18, wait[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(side, 66, 589 + i * 44, 174, 15, wait[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
await pill(side, 242, 576 + i * 44, 54, 24, 'Fit', C.wine700, C.roseSoft);
|
||
}
|
||
}
|
||
|
||
async function bookingBlock(parent, x, y, w, h, title, person, color) {
|
||
const b = frame(parent, x, y, w, h, color, 0.10, 14, 'Booking ' + title);
|
||
addStroke(b, color, 0.24);
|
||
rect(b, 0, 0, 4, h, color, 1, 3, 'Booking accent');
|
||
await txt(b, 10, 10, w - 18, 30, title, 11, color, 'bold', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(b, 10, h - 30, w - 18, 18, person, 10, C.text2, 'semibold', 'LEFT');
|
||
}
|
||
|
||
// Page 06
|
||
async function pageClientCRM(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const list = card(root, x, y, 342, 720, 30, 'Client list');
|
||
await sectionHeader(list, 22, 22, 298, 'Client intelligence', 'Import');
|
||
const search = frame(list, 22, 66, 298, 44, C.cream, 0.74, 15, 'Client search');
|
||
addStroke(search, C.border, 0.85); iconSearch(search, 14, 13, C.muted);
|
||
await txt(search, 46, 0, 210, 44, 'Search VIP clients', 12, C.muted, 'medium', 'LEFT');
|
||
const clients = [
|
||
['Ariana Reed', 'Diamond · $8.4k', 'AR', C.gold],
|
||
['Mia Chen', 'Gold · $5.2k', 'MC', C.rose],
|
||
['Sofia King', 'Color lover', 'SK', C.wine600],
|
||
['Nora Bell', 'Lash member', 'NB', C.violet],
|
||
['Olivia West', 'New client', 'OW', C.blue],
|
||
['Vera Stone', 'Bride journey', 'VS', C.success]
|
||
];
|
||
for (let i = 0; i < clients.length; i++) {
|
||
const yy = 132 + i * 84;
|
||
const selected = i === 0;
|
||
const row = frame(list, 18, yy, 306, 68, selected ? C.roseSoft : C.surface, selected ? 0.9 : 0.36, 20, 'Client row');
|
||
addStroke(row, selected ? C.rose : C.border, selected ? 0.32 : 0.5);
|
||
await avatar(row, 14, 14, 40, clients[i][2], clients[i][3], C.wine700);
|
||
await txt(row, 66, 12, 150, 20, clients[i][0], 13, C.text, 'bold', 'LEFT');
|
||
await txt(row, 66, 34, 150, 18, clients[i][1], 11, C.muted, 'medium', 'LEFT');
|
||
await pill(row, 224, 20, 58, 26, selected ? 'Open' : 'View', selected ? C.surface : C.wine700, selected ? C.wine700 : C.roseSoft);
|
||
}
|
||
|
||
const profile = luxuryHero(root, x + 370, y, 500, 310, 'Client profile hero');
|
||
await avatar(profile, 30, 34, 86, 'AR', C.gold, C.wine700);
|
||
await txt(profile, 132, 36, 240, 32, 'Ariana Reed', 28, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(profile, 132, 74, 280, 22, 'Diamond member · Loves rose gold color and hydrating facials', 12, C.rose2, 'medium', 'LEFT');
|
||
await pill(profile, 132, 112, 120, 28, 'No-show risk 2%', C.success, C.successSoft);
|
||
await pill(profile, 262, 112, 126, 28, 'Birthday in 14d', C.goldDark, C.gold2);
|
||
await labelValue(profile, 34, 178, 'Lifetime value', '$8,420', C.surface);
|
||
await labelValue(profile, 190, 178, 'Visits', '28', C.surface);
|
||
await labelValue(profile, 318, 178, 'Favorite stylist', 'Olivia', C.surface);
|
||
lineChart(profile, 34, 250, 420, 38, [18, 24, 22, 35, 42, 39, 53, 58], C.gold2);
|
||
|
||
const notes = card(root, x + 898, y, 198, 310, 28, 'Client notes');
|
||
await sectionHeader(notes, 18, 20, 162, 'Care notes', null);
|
||
const noteItems = ['Sensitive scalp', 'Prefers quiet suite', 'Rose gold toner 8RG', 'Offer luxe mask add-on'];
|
||
for (let i = 0; i < noteItems.length; i++) {
|
||
rect(notes, 20, 66 + i * 48, 158, 34, i % 2 ? C.cream : C.roseSoft, 0.8, 13, 'Note item');
|
||
await txt(notes, 32, 66 + i * 48, 134, 34, noteItems[i], 11, C.text2, 'semibold', 'LEFT');
|
||
}
|
||
|
||
const timeline = card(root, x + 370, y + 340, 500, 380, 28, 'Client timeline');
|
||
await sectionHeader(timeline, 24, 20, 452, 'Visit timeline', 'Add note');
|
||
const events = [['Today', 'Rose Gold Color Ritual', '$420 · Olivia'], ['May 22', 'Hydrafacial Elite', '$260 · Naomi'], ['Apr 30', 'Luxury blowout', '$95 · Lily'], ['Apr 12', 'Retail purchase', '$180 · Kerastase set']];
|
||
for (let i = 0; i < events.length; i++) {
|
||
const yy = 78 + i * 72;
|
||
ellipse(timeline, 28, yy + 6, 16, 16, i === 0 ? C.gold : C.rose, 1, 'Timeline dot');
|
||
if (i < events.length - 1) rect(timeline, 35, yy + 22, 2, 48, C.borderWine, 1, 1, 'Timeline rail');
|
||
await txt(timeline, 58, yy - 2, 84, 20, events[i][0], 11, C.goldDark, 'bold', 'LEFT');
|
||
await txt(timeline, 150, yy - 2, 220, 20, events[i][1], 13, C.text, 'bold', 'LEFT');
|
||
await txt(timeline, 150, yy + 24, 220, 18, events[i][2], 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
|
||
const segments = card(root, x + 898, y + 340, 198, 380, 28, 'Segments');
|
||
await sectionHeader(segments, 18, 20, 162, 'Segments', null);
|
||
await progressBar(segments, 22, 74, 154, 'Diamond VIP', 92, C.goldDark);
|
||
await progressBar(segments, 22, 138, 154, 'Color lovers', 88, C.wine700);
|
||
await progressBar(segments, 22, 202, 154, 'Facial buyers', 71, C.rose);
|
||
await progressBar(segments, 22, 266, 154, 'Retail likely', 64, C.success);
|
||
await pill(segments, 32, 332, 134, 30, 'Send campaign', C.surface, C.wine700);
|
||
}
|
||
|
||
// Page 07
|
||
async function pageServices(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const hero = luxuryHero(root, x, y, 1096, 186, 'Services hero');
|
||
await txt(hero, 32, 28, 180, 18, 'SERVICE STRATEGY', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(hero, 32, 56, 460, 42, 'Curated luxury service menu with margin control.', 34, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(hero, 32, 112, 520, 24, 'Design signature experiences, add-ons, packages, duration rules, and stylist skill mapping.', 13, C.rose2, 'medium', 'LEFT');
|
||
await labelValue(hero, 710, 44, 'Average margin', '72%', C.surface);
|
||
await labelValue(hero, 850, 44, 'Top package', 'Luxe Glow Ritual', C.surface);
|
||
barChart(hero, 690, 106, 340, 44, [60, 86, 72, 94, 64, 76, 88], C.gold2, C.surface);
|
||
|
||
const categories = card(root, x, y + 216, 324, 504, 28, 'Service categories');
|
||
await sectionHeader(categories, 22, 20, 280, 'Categories', 'New');
|
||
const cats = [['Hair Color', '$184 avg', 92, C.wine700], ['Skin Clinic', '$240 avg', 81, C.rose], ['Nails Atelier', '$88 avg', 63, C.goldDark], ['Brows & Lashes', '$76 avg', 70, C.violet], ['Bridal Lounge', '$620 avg', 55, C.blue]];
|
||
for (let i = 0; i < cats.length; i++) {
|
||
const yy = 70 + i * 80;
|
||
const row = frame(categories, 20, yy, 284, 64, i === 0 ? C.roseSoft : C.cream, i === 0 ? 0.92 : 0.58, 18, 'Category row');
|
||
addStroke(row, cats[i][3], i === 0 ? 0.28 : 0.08);
|
||
iconChip(row, 14, 16, 'services', cats[i][3], cats[i][3], 0.12, 32);
|
||
await txt(row, 58, 12, 126, 20, cats[i][0], 13, C.text, 'bold', 'LEFT');
|
||
await txt(row, 58, 34, 90, 16, cats[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
await progressBar(row, 178, 15, 86, '', cats[i][2], cats[i][3]);
|
||
}
|
||
|
||
const matrix = card(root, x + 352, y + 216, 456, 504, 28, 'Pricing matrix');
|
||
await sectionHeader(matrix, 24, 20, 408, 'Signature pricing matrix', 'Publish');
|
||
await table(matrix, 24, 74, 408, ['Service', 'Duration', 'Price', 'Margin'], [
|
||
['Rose Gold Color', '120m', '$420', '78%'],
|
||
['Diamond Hydrafacial', '75m', '$260', '72%'],
|
||
['Keratin Luxe', '150m', '$380', '68%'],
|
||
['Bridal Preview', '180m', '$640', '81%'],
|
||
['Gloss & Blowout', '70m', '$150', '74%'],
|
||
['Brow Architecture', '45m', '$92', '69%']
|
||
], [150, 82, 82, 78], 46);
|
||
|
||
const builder = card(root, x + 836, y + 216, 260, 504, 28, 'Package builder');
|
||
await sectionHeader(builder, 22, 20, 216, 'Package builder', null);
|
||
const image = luxuryHero(builder, 22, 64, 216, 132, 'Package visual');
|
||
await txt(image, 18, 22, 150, 26, 'Luxe Glow Ritual', 23, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(image, 18, 58, 150, 32, 'Color, facial, blowout, and private suite.', 11, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
const addon = [['Color consult', '$45'], ['Luxury mask', '$38'], ['Scalp massage', '$28'], ['Retail kit', '$120']];
|
||
for (let i = 0; i < addon.length; i++) {
|
||
const yy = 220 + i * 52;
|
||
rect(builder, 22, yy, 216, 40, C.cream, 0.68, 14, 'Addon row');
|
||
await txt(builder, 36, yy, 124, 40, addon[i][0], 12, C.text2, 'semibold', 'LEFT');
|
||
await txt(builder, 174, yy, 50, 40, addon[i][1], 12, C.goldDark, 'bold', 'RIGHT');
|
||
}
|
||
await pill(builder, 38, 448, 184, 34, 'Preview service page', C.surface, C.wine700);
|
||
}
|
||
|
||
// Page 08
|
||
async function pageStaff(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const roster = card(root, x, y, 676, 336, 30, 'Staff roster');
|
||
await sectionHeader(roster, 24, 22, 628, 'Specialist roster', 'Invite');
|
||
const staff = [['Olivia Hart', 'Color Director', '94%', 'OH', C.wine700], ['Naomi Vale', 'Skin Expert', '89%', 'NV', C.rose], ['Lily Park', 'Stylist', '86%', 'LP', C.goldDark], ['Vera Bloom', 'Bridal Lead', '91%', 'VB', C.violet]];
|
||
for (let i = 0; i < staff.length; i++) {
|
||
const sx = 24 + i * 158;
|
||
const s = glass(roster, sx, 76, 138, 224, 22, 'Staff card');
|
||
await avatar(s, 35, 18, 68, staff[i][3], staff[i][4], C.surface);
|
||
await txt(s, 12, 96, 114, 22, staff[i][0], 13, C.text, 'bold', 'CENTER');
|
||
await txt(s, 12, 120, 114, 17, staff[i][1], 10, C.muted, 'medium', 'CENTER');
|
||
await progressBar(s, 16, 154, 106, 'Rating', parseInt(staff[i][2]), staff[i][4]);
|
||
await pill(s, 24, 192, 90, 24, 'View profile', C.wine700, C.roseSoft);
|
||
}
|
||
|
||
const commission = luxuryHero(root, x + 704, y, 392, 336, 'Commission hero');
|
||
await txt(commission, 28, 28, 200, 18, 'COMMISSION FORECAST', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(commission, 28, 58, 210, 50, '$18,420', 44, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(commission, 28, 120, 256, 24, 'Projected team payout for the current pay period.', 12, C.rose2, 'medium', 'LEFT');
|
||
lineChart(commission, 34, 190, 300, 76, [20, 28, 25, 36, 42, 55, 49, 62], C.gold2);
|
||
await pill(commission, 28, 278, 128, 30, '+12.6% vs last', C.goldDark, C.gold2);
|
||
|
||
const schedule = card(root, x, y + 368, 724, 352, 28, 'Staff schedule');
|
||
await sectionHeader(schedule, 24, 20, 676, 'Shift coverage map', 'Adjust');
|
||
const names = ['Olivia', 'Naomi', 'Lily', 'Vera', 'Maya'];
|
||
const times = ['9 AM', '11 AM', '1 PM', '3 PM', '5 PM', '7 PM'];
|
||
for (let i = 0; i < times.length; i++) await txt(schedule, 132 + i * 86, 66, 62, 16, times[i], 10, C.faint, 'bold', 'CENTER');
|
||
for (let r = 0; r < names.length; r++) {
|
||
await txt(schedule, 28, 96 + r * 46, 78, 24, names[r], 12, C.text, 'bold', 'LEFT');
|
||
rect(schedule, 120, 106 + r * 46, 520, 8, C.cream2, 1, 4, 'Shift track');
|
||
rect(schedule, 120 + (r % 3) * 40, 100 + r * 46, 230 + r * 24, 20, [C.wine700, C.rose, C.goldDark, C.violet, C.success][r], 0.82, 10, 'Shift block');
|
||
}
|
||
await txt(schedule, 28, 310, 620, 20, 'Coverage is strongest for color services. Facial room capacity drops after 5 PM.', 12, C.muted, 'medium', 'LEFT');
|
||
|
||
const leaderboard = card(root, x + 752, y + 368, 344, 352, 28, 'Performance leaderboard');
|
||
await sectionHeader(leaderboard, 22, 20, 300, 'Performance board', null);
|
||
const leaders = [['Olivia', '$24.8k', 96], ['Vera', '$19.2k', 88], ['Naomi', '$17.6k', 82], ['Lily', '$14.4k', 74]];
|
||
for (let i = 0; i < leaders.length; i++) {
|
||
await txt(leaderboard, 26, 78 + i * 62, 34, 26, '#' + (i + 1), 14, i === 0 ? C.goldDark : C.muted, 'bold', 'LEFT');
|
||
await txt(leaderboard, 64, 76 + i * 62, 110, 18, leaders[i][0], 13, C.text, 'bold', 'LEFT');
|
||
await txt(leaderboard, 64, 98 + i * 62, 90, 16, leaders[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
rect(leaderboard, 190, 84 + i * 62, 110, 8, C.cream2, 1, 4, 'Score track');
|
||
rect(leaderboard, 190, 84 + i * 62, 110 * leaders[i][2] / 100, 8, i === 0 ? C.goldDark : C.wine700, 1, 4, 'Score fill');
|
||
}
|
||
}
|
||
|
||
// Page 09
|
||
async function pageInventory(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const alerts = card(root, x, y, 278, 720, 30, 'Stock alerts');
|
||
await sectionHeader(alerts, 22, 22, 234, 'Stock alerts', null);
|
||
const alertData = [['Keratin Mask', '2 days left', C.danger, C.dangerSoft], ['Rose Toner', '4 bottles', C.warning, C.warningSoft], ['Lash Adhesive', 'Low temp stock', C.warning, C.warningSoft], ['Retail Serum', 'High demand', C.success, C.successSoft]];
|
||
for (let i = 0; i < alertData.length; i++) {
|
||
const box = frame(alerts, 20, 74 + i * 92, 238, 72, alertData[i][3], 0.85, 20, 'Alert');
|
||
addStroke(box, alertData[i][2], 0.16);
|
||
iconChip(box, 16, 18, 'inventory', alertData[i][2], alertData[i][2], 0.13, 36);
|
||
await txt(box, 64, 14, 110, 20, alertData[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(box, 64, 38, 120, 18, alertData[i][1], 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
await pill(alerts, 42, 640, 194, 34, 'Create purchase order', C.surface, C.wine700);
|
||
|
||
const shelf = card(root, x + 306, y, 514, 394, 30, 'Product shelf');
|
||
await sectionHeader(shelf, 24, 22, 466, 'Retail product shelf', 'Manage');
|
||
const products = [['Luxe Repair Mask', '$58', C.rose], ['Gold Hair Oil', '$42', C.gold], ['Hydra Serum', '$86', C.blue], ['Brow Laminate Kit', '$34', C.violet], ['Color Care Duo', '$74', C.wine600], ['Spa Candle', '$28', C.success]];
|
||
for (let i = 0; i < products.length; i++) {
|
||
const px = 24 + (i % 3) * 156;
|
||
const py = 80 + Math.floor(i / 3) * 140;
|
||
const p = glass(shelf, px, py, 134, 116, 20, 'Product');
|
||
ellipse(p, 46, 14, 42, 42, products[i][2], 0.16, 'Product glow');
|
||
rect(p, 54, 22, 26, 46, products[i][2], 0.7, 8, 'Bottle');
|
||
rect(p, 59, 16, 16, 8, products[i][2], 0.95, 3, 'Cap');
|
||
await txt(p, 12, 72, 110, 17, products[i][0], 10, C.text, 'bold', 'CENTER');
|
||
await txt(p, 12, 92, 110, 16, products[i][1], 11, C.goldDark, 'bold', 'CENTER');
|
||
}
|
||
|
||
const revenue = luxuryHero(root, x + 848, y, 248, 394, 'Retail revenue');
|
||
await txt(revenue, 24, 26, 160, 18, 'RETAIL REVENUE', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(revenue, 24, 56, 170, 42, '$31,480', 36, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(revenue, 24, 108, 180, 34, 'Retail attach rate rose after checkout recommendations.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
barChart(revenue, 28, 190, 188, 95, [22, 45, 38, 66, 58, 82, 76], C.gold2, C.surface);
|
||
await pill(revenue, 36, 326, 168, 30, '+18% attach rate', C.goldDark, C.gold2);
|
||
|
||
const reorder = card(root, x + 306, y + 424, 790, 296, 28, 'Reorder table');
|
||
await sectionHeader(reorder, 24, 20, 742, 'Purchase order queue', 'Send orders');
|
||
await table(reorder, 24, 74, 742, ['Supplier', 'Items', 'Cost', 'Status', 'ETA'], [
|
||
['Oribe Pro', '12 SKUs', '$2,840', 'Ready', 'Jun 14'],
|
||
['SkinCeuticals', '8 SKUs', '$3,120', 'Approval', 'Jun 18'],
|
||
['Lash Studio', '5 SKUs', '$780', 'Draft', 'Jun 12'],
|
||
['BeautyHub Retail', '16 SKUs', '$4,600', 'Ready', 'Jun 20']
|
||
], [190, 130, 120, 140, 120], 46);
|
||
}
|
||
|
||
// Page 10
|
||
async function pagePayments(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const flow = luxuryHero(root, x, y, 470, 310, 'Payment flow');
|
||
await txt(flow, 30, 28, 160, 18, 'PAYMENT FLOW', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(flow, 30, 58, 260, 46, '$86,240', 42, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(flow, 30, 114, 300, 22, 'Net processed value across card, deposit, membership, and invoice payments.', 12, C.rose2, 'medium', 'LEFT');
|
||
const steps = [['Deposit', 82], ['Checkout', 94], ['Tips', 76], ['Settlement', 88]];
|
||
for (let i = 0; i < steps.length; i++) await progressBar(flow, 34, 170 + i * 32, 360, steps[i][0], steps[i][1], C.gold2);
|
||
|
||
const settlement = card(root, x + 500, y, 296, 310, 28, 'Settlement');
|
||
await sectionHeader(settlement, 22, 22, 252, 'Next settlement', null);
|
||
ellipse(settlement, 80, 86, 136, 136, C.gold, 0.16, 'Donut outer');
|
||
ellipse(settlement, 112, 118, 72, 72, C.surface, 1, 'Donut center');
|
||
await txt(settlement, 76, 132, 144, 30, '$18.9k', 26, C.text, 'bold', 'CENTER', 'display');
|
||
await txt(settlement, 66, 166, 164, 18, 'arrives tomorrow', 11, C.muted, 'medium', 'CENTER');
|
||
await pill(settlement, 54, 250, 188, 34, 'View payout report', C.surface, C.wine700);
|
||
|
||
const methods = card(root, x + 824, y, 272, 310, 28, 'Payment methods');
|
||
await sectionHeader(methods, 22, 22, 228, 'Payment mix', null);
|
||
const mix = [['Card', 64, C.wine700], ['Apple Pay', 18, C.ink], ['Membership', 11, C.goldDark], ['Cash', 7, C.muted]];
|
||
for (let i = 0; i < mix.length; i++) await progressBar(methods, 24, 78 + i * 54, 224, mix[i][0], mix[i][1], mix[i][2]);
|
||
|
||
const transactions = card(root, x, y + 340, 700, 380, 28, 'Transactions');
|
||
await sectionHeader(transactions, 24, 20, 652, 'Recent transactions', 'Download CSV');
|
||
await table(transactions, 24, 76, 652, ['Client', 'Service', 'Method', 'Amount', 'Status'], [
|
||
['Sofia King', 'Rose Gold Color', 'Visa • 4882', '$420', 'Paid'],
|
||
['Ariana Reed', 'Diamond Facial', 'Apple Pay', '$260', 'Paid'],
|
||
['Nora Bell', 'Lash Lift', 'Membership', '$0', 'Included'],
|
||
['Vera Stone', 'Bridal Trial', 'Deposit', '$300', 'Captured'],
|
||
['Mia Chen', 'Gloss Treatment', 'Visa • 1028', '$150', 'Paid']
|
||
], [140, 170, 132, 92, 90], 48);
|
||
|
||
const invoices = card(root, x + 728, y + 340, 368, 380, 28, 'Invoice queue');
|
||
await sectionHeader(invoices, 22, 20, 324, 'Invoice queue', 'Create');
|
||
const inv = [['INV-2048', 'Corporate glam event', '$4,800'], ['INV-2049', 'Bridal party', '$2,640'], ['INV-2050', 'Monthly membership', '$1,120']];
|
||
for (let i = 0; i < inv.length; i++) {
|
||
const row = glass(invoices, 22, 76 + i * 86, 324, 66, 18, 'Invoice row');
|
||
await txt(row, 16, 11, 90, 18, inv[i][0], 12, C.wine700, 'bold', 'LEFT');
|
||
await txt(row, 16, 34, 170, 17, inv[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
await txt(row, 220, 20, 78, 22, inv[i][2], 16, C.text, 'bold', 'RIGHT', 'display');
|
||
}
|
||
await pill(invoices, 84, 320, 200, 34, 'Open checkout console', C.surface, C.wine700);
|
||
}
|
||
|
||
// Page 11
|
||
async function pageMarketing(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const canvas = card(root, x, y, 614, 720, 30, 'Campaign canvas');
|
||
await sectionHeader(canvas, 24, 22, 566, 'Campaign automation canvas', 'Launch');
|
||
const trigger = await flowNode(canvas, 54, 96, 'Trigger', 'Client has not visited in 45 days', C.wine700);
|
||
const segment = await flowNode(canvas, 314, 96, 'Segment', 'Color clients with spend above $300', C.rose);
|
||
const offer = await flowNode(canvas, 184, 246, 'Offer', 'Rose Gold Ritual with 15% VIP upgrade', C.goldDark);
|
||
const email = await flowNode(canvas, 54, 404, 'Email', 'Personalized invitation with stylist quote', C.violet);
|
||
const sms = await flowNode(canvas, 314, 404, 'SMS', 'Reminder 24 hours after email open', C.blue);
|
||
line(canvas, 236, 138, 314, 138, C.borderWine, 1, 2, 'Flow connector');
|
||
line(canvas, 184, 184, 242, 246, C.borderWine, 1, 2, 'Flow connector');
|
||
line(canvas, 392, 184, 320, 246, C.borderWine, 1, 2, 'Flow connector');
|
||
line(canvas, 244, 330, 134, 404, C.borderWine, 1, 2, 'Flow connector');
|
||
line(canvas, 320, 330, 394, 404, C.borderWine, 1, 2, 'Flow connector');
|
||
const stats = glass(canvas, 54, 584, 506, 82, 22, 'Campaign forecast');
|
||
await labelValue(stats, 22, 18, 'Audience', '1,842 clients', C.text);
|
||
await labelValue(stats, 190, 18, 'Expected bookings', '214', C.success);
|
||
await labelValue(stats, 346, 18, 'Projected revenue', '$38.6k', C.wine700);
|
||
|
||
const preview = luxuryHero(root, x + 642, y, 454, 420, 'Email preview');
|
||
await txt(preview, 30, 28, 160, 18, 'CREATIVE PREVIEW', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(preview, 30, 58, 280, 42, 'Your Rose Gold Ritual awaits.', 34, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(preview, 30, 118, 300, 46, 'A personalized treatment invitation styled for premium clients and luxury beauty centers.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
const mail = glass(preview, 54, 194, 346, 170, 24, 'Email card');
|
||
await txt(mail, 24, 22, 220, 22, 'Luxe Beauty House', 16, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(mail, 24, 58, 260, 42, 'Ariana, Olivia reserved a private color suite for you this week.', 13, C.text2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(mail, 24, 116, 156, 32, 'Reserve my ritual', C.surface, C.wine700);
|
||
|
||
const audience = card(root, x + 642, y + 448, 454, 272, 28, 'Audience segments');
|
||
await sectionHeader(audience, 24, 20, 406, 'Audience segments', null);
|
||
await progressBar(audience, 28, 78, 390, 'High-value color clients', 84, C.wine700);
|
||
await progressBar(audience, 28, 138, 390, 'Dormant VIP members', 62, C.goldDark);
|
||
await progressBar(audience, 28, 198, 390, 'Facial buyers', 48, C.rose);
|
||
}
|
||
|
||
async function flowNode(parent, x, y, title, body, color) {
|
||
const n = glass(parent, x, y, 182, 88, 22, 'Flow ' + title);
|
||
iconChip(n, 14, 16, 'marketing', color, color, 0.12, 34);
|
||
await txt(n, 58, 15, 100, 20, title, 12, color, 'bold', 'LEFT');
|
||
await txt(n, 58, 38, 102, 36, body, 10, C.text2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
return n;
|
||
}
|
||
|
||
// Page 12
|
||
async function pageReviews(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const rating = luxuryHero(root, x, y, 336, 720, 'Rating hero');
|
||
await txt(rating, 28, 30, 170, 18, 'REPUTATION HEALTH', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(rating, 28, 68, 180, 56, '4.92', 58, C.surface, 'heavy', 'LEFT', 'display');
|
||
await txt(rating, 184, 86, 100, 24, '/ 5.0', 18, C.rose2, 'bold', 'LEFT', 'display');
|
||
await txt(rating, 28, 142, 250, 38, '2,184 verified reviews across Google, Fresha, and direct bookings.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
const ratings = [['5 star', 92], ['4 star', 6], ['3 star', 1], ['2 star', 1], ['1 star', 0]];
|
||
for (let i = 0; i < ratings.length; i++) await progressBar(rating, 30, 230 + i * 48, 260, ratings[i][0], ratings[i][1], C.gold2);
|
||
await pill(rating, 50, 610, 218, 36, 'Open response center', C.goldDark, C.gold2);
|
||
|
||
const feed = card(root, x + 364, y, 450, 720, 30, 'Review feed');
|
||
await sectionHeader(feed, 24, 22, 402, 'Live review feed', 'Filter');
|
||
const reviews = [
|
||
['Ariana Reed', 'The most elegant color experience I have ever had.', 'Google', C.gold],
|
||
['Mia Chen', 'The private suite and hospitality felt truly premium.', 'Fresha', C.rose],
|
||
['Vera Stone', 'My bridal trial was organized perfectly from start to finish.', 'Direct', C.wine600],
|
||
['Nora Bell', 'Clean, calm, beautiful salon. Booking was effortless.', 'Google', C.violet]
|
||
];
|
||
for (let i = 0; i < reviews.length; i++) {
|
||
const r = glass(feed, 24, 76 + i * 146, 402, 116, 22, 'Review card');
|
||
await avatar(r, 18, 18, 40, reviews[i][0].split(' ').map(s => s[0]).join(''), reviews[i][3], C.wine700);
|
||
await txt(r, 70, 14, 160, 20, reviews[i][0], 13, C.text, 'bold', 'LEFT');
|
||
await txt(r, 70, 38, 250, 38, reviews[i][1], 12, C.text2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(r, 316, 16, 64, 24, reviews[i][2], C.wine700, C.roseSoft);
|
||
await txt(r, 70, 82, 110, 18, '★★★★★', 11, C.goldDark, 'bold', 'LEFT');
|
||
}
|
||
|
||
const assistant = card(root, x + 842, y, 254, 344, 28, 'Response assistant');
|
||
await sectionHeader(assistant, 20, 20, 214, 'Response assistant', null);
|
||
const msg = frame(assistant, 20, 76, 214, 174, C.cream, 0.78, 20, 'Response draft');
|
||
addStroke(msg, C.border, 0.8);
|
||
await txt(msg, 16, 16, 180, 124, 'Thank you, Ariana. We loved creating your rose gold transformation and will share your kind words with Olivia and the color team.', 12, C.text2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(assistant, 34, 282, 82, 30, 'Refine', C.wine700, C.roseSoft);
|
||
await pill(assistant, 126, 282, 92, 30, 'Publish', C.surface, C.wine700);
|
||
|
||
const sentiment = card(root, x + 842, y + 374, 254, 346, 28, 'Sentiment');
|
||
await sectionHeader(sentiment, 20, 20, 214, 'Sentiment drivers', null);
|
||
await progressBar(sentiment, 24, 82, 206, 'Hospitality', 96, C.goldDark);
|
||
await progressBar(sentiment, 24, 146, 206, 'Design quality', 89, C.wine700);
|
||
await progressBar(sentiment, 24, 210, 206, 'Booking ease', 84, C.success);
|
||
await progressBar(sentiment, 24, 274, 206, 'Wait time', 38, C.warning);
|
||
}
|
||
|
||
// Page 13
|
||
async function pageAnalytics(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const topW = (CONTENT_W - 3 * 18) / 4;
|
||
await metricCard(root, x, y, topW, 126, 'Forecast revenue', '$192.4k', '+16.8%', 'analytics', C.wine700);
|
||
await metricCard(root, x + topW + 18, y, topW, 126, 'Booking probability', '82%', '+7.4%', 'calendar', C.goldDark);
|
||
await metricCard(root, x + 2 * (topW + 18), y, topW, 126, 'Client retention', '68%', '+5.1%', 'clients', C.success);
|
||
await metricCard(root, x + 3 * (topW + 18), y, topW, 126, 'Churn risk', '7.8%', '-1.9%', 'reviews', C.rose);
|
||
|
||
const main = card(root, x, y + 156, 628, 374, 30, 'Executive BI');
|
||
await sectionHeader(main, 24, 22, 580, 'Executive growth model', 'Board view');
|
||
lineChart(main, 44, 112, 520, 160, [62, 68, 64, 76, 84, 82, 94, 101, 110, 118, 132, 146], C.wine700);
|
||
await labelValue(main, 44, 304, 'Best forecast window', 'Thu–Sat afternoons', C.text);
|
||
await labelValue(main, 282, 304, 'Capacity upside', '+$24.8k', C.success);
|
||
await labelValue(main, 464, 304, 'Confidence', '91%', C.goldDark);
|
||
|
||
const heat = card(root, x + 656, y + 156, 440, 374, 30, 'Demand heatmap');
|
||
await sectionHeader(heat, 24, 22, 392, 'Demand heatmap', null);
|
||
const rows = ['Hair', 'Skin', 'Nails', 'Lashes', 'Bridal'];
|
||
const cols = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||
for (let i = 0; i < cols.length; i++) await txt(heat, 100 + i * 50, 72, 40, 16, cols[i], 10, C.muted, 'bold', 'CENTER');
|
||
for (let r = 0; r < rows.length; r++) {
|
||
await txt(heat, 24, 106 + r * 44, 64, 18, rows[r], 10, C.text2, 'bold', 'LEFT');
|
||
for (let c = 0; c < cols.length; c++) {
|
||
const val = ((r + 2) * (c + 3) * 17) % 100;
|
||
const col = val > 70 ? C.wine700 : val > 45 ? C.rose : C.champagne;
|
||
rect(heat, 104 + c * 50, 100 + r * 44, 34, 28, col, val > 70 ? 0.88 : val > 45 ? 0.62 : 0.9, 9, 'Heat cell');
|
||
}
|
||
}
|
||
await txt(heat, 24, 326, 360, 18, 'Friday color appointments and Saturday bridal services are the highest yield windows.', 11, C.muted, 'medium', 'LEFT');
|
||
|
||
const cohort = card(root, x, y + 560, 526, 284, 28, 'Cohorts');
|
||
await sectionHeader(cohort, 24, 20, 478, 'Client cohort retention', 'Explore');
|
||
barChart(cohort, 42, 92, 420, 116, [70, 64, 58, 52, 47, 42, 39, 35], C.wine700, C.border);
|
||
await txt(cohort, 44, 230, 430, 18, 'VIP onboarding improves month-three retention by 22%.', 12, C.text2, 'semibold', 'LEFT');
|
||
|
||
const insight = card(root, x + 554, y + 560, 542, 284, 28, 'Forecast insights');
|
||
await sectionHeader(insight, 24, 20, 494, 'Forecast insights', 'Share');
|
||
const insights = [['Raise Friday color suite capacity', '+$12.6k potential'], ['Bundle facials with retail serum', '+18% attach'], ['Reactivate dormant VIPs', '214 likely bookings']];
|
||
for (let i = 0; i < insights.length; i++) {
|
||
const row = glass(insight, 24, 76 + i * 62, 494, 48, 16, 'Insight');
|
||
await txt(row, 18, 0, 290, 48, insights[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(row, 330, 0, 140, 48, insights[i][1], 12, C.success, 'bold', 'RIGHT');
|
||
}
|
||
}
|
||
|
||
// Page 14
|
||
async function pageBranches(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const map = luxuryHero(root, x, y, 530, 438, 'Branch map');
|
||
await txt(map, 30, 28, 170, 18, 'MULTI-LOCATION VIEW', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(map, 30, 60, 330, 42, 'Three flagship beauty centers performing above plan.', 32, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(map, 30, 116, 330, 38, 'Regional dashboard for branch health, occupancy, and guest experience.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
// Abstract city map
|
||
for (let i = 0; i < 9; i++) line(map, 46 + i * 52, 218, 92 + i * 42, 388, C.surface, 0.09, 10, 'Map road');
|
||
const pins = [[180, 260, 'Downtown'], [314, 216, 'Westside'], [388, 330, 'Marina']];
|
||
for (let i = 0; i < pins.length; i++) {
|
||
ellipse(map, pins[i][0] - 16, pins[i][1] - 16, 32, 32, C.gold, 0.28, 'Map pin glow');
|
||
ellipse(map, pins[i][0] - 7, pins[i][1] - 7, 14, 14, C.gold2, 1, 'Map pin');
|
||
await txt(map, pins[i][0] - 44, pins[i][1] + 18, 88, 16, pins[i][2], 10, C.surface, 'bold', 'CENTER');
|
||
}
|
||
|
||
const cards = card(root, x + 558, y, 538, 438, 30, 'Branch cards');
|
||
await sectionHeader(cards, 24, 22, 490, 'Branch health cards', 'Add branch');
|
||
const branches = [['Downtown Atelier', '$64.2k', 92, C.wine700], ['Westside Spa', '$48.8k', 84, C.goldDark], ['Marina Beauty Club', '$39.6k', 78, C.rose]];
|
||
for (let i = 0; i < branches.length; i++) {
|
||
const b = glass(cards, 24, 78 + i * 106, 490, 82, 22, 'Branch card');
|
||
iconChip(b, 18, 22, 'branches', branches[i][3], branches[i][3], 0.14, 38);
|
||
await txt(b, 70, 16, 190, 22, branches[i][0], 14, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(b, 70, 42, 110, 18, branches[i][1] + ' revenue', 11, C.muted, 'medium', 'LEFT');
|
||
await progressBar(b, 292, 18, 160, 'Health', branches[i][2], branches[i][3]);
|
||
}
|
||
|
||
const compare = card(root, x, y + 468, 642, 252, 28, 'Comparison table');
|
||
await sectionHeader(compare, 24, 20, 594, 'Location comparison', 'Export');
|
||
await table(compare, 24, 74, 594, ['Branch', 'Occupancy', 'Revenue', 'Rating', 'Manager'], [
|
||
['Downtown', '92%', '$64.2k', '4.94', 'Emma Lane'],
|
||
['Westside', '84%', '$48.8k', '4.89', 'Maya Cole'],
|
||
['Marina', '78%', '$39.6k', '4.86', 'Nora Field']
|
||
], [130, 110, 110, 90, 122], 44);
|
||
|
||
const occupancy = card(root, x + 670, y + 468, 426, 252, 28, 'Occupancy');
|
||
await sectionHeader(occupancy, 24, 20, 378, 'Occupancy by branch', null);
|
||
barChart(occupancy, 40, 94, 336, 92, [92, 84, 78, 66, 58, 72], C.wine700, C.border);
|
||
await txt(occupancy, 42, 210, 334, 18, 'Downtown should receive overflow bookings from Marina on Saturday.', 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
|
||
// Page 15
|
||
async function pageSettings(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const brand = card(root, x, y, 382, 720, 30, 'Brand settings');
|
||
await sectionHeader(brand, 24, 22, 334, 'Brand system', 'Save');
|
||
const preview = luxuryHero(brand, 24, 74, 334, 154, 'Brand preview');
|
||
await txt(preview, 24, 24, 180, 28, 'Luxe Beauty House', 26, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(preview, 24, 62, 230, 34, 'Luxury salon and beauty center dashboard identity.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await formField(brand, 24, 258, 334, 'Business name', 'Luxe Beauty House Center');
|
||
await formField(brand, 24, 334, 334, 'Brand tone', 'Elegant, warm, premium, and calm');
|
||
await formField(brand, 24, 410, 334, 'Default booking policy', 'Deposits required for services above $250', true);
|
||
await txt(brand, 24, 526, 180, 20, 'Luxury palette', 14, C.text, 'bold', 'LEFT', 'display');
|
||
const swatches = [C.wine700, C.wine500, C.rose, C.gold, C.cream2, C.ink];
|
||
for (let i = 0; i < swatches.length; i++) {
|
||
const s = rect(brand, 24 + i * 54, 562, 42, 42, swatches[i], 1, 14, 'Color swatch');
|
||
addStroke(s, C.surface, 0.9, 2);
|
||
}
|
||
|
||
const roles = card(root, x + 410, y, 370, 338, 28, 'Roles permissions');
|
||
await sectionHeader(roles, 24, 20, 322, 'Roles & permissions', 'Edit');
|
||
const perms = [['Owner', 'Full access', C.goldDark], ['Manager', 'Operations + reports', C.wine700], ['Stylist', 'Bookings + clients', C.rose], ['Reception', 'Calendar + checkout', C.violet]];
|
||
for (let i = 0; i < perms.length; i++) {
|
||
const row = frame(roles, 24, 74 + i * 58, 322, 42, C.cream, 0.62, 15, 'Role row');
|
||
addStroke(row, C.border, 0.6);
|
||
ellipse(row, 16, 13, 16, 16, perms[i][2], 0.9, 'Role dot');
|
||
await txt(row, 42, 0, 90, 42, perms[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(row, 142, 0, 160, 42, perms[i][1], 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
|
||
const integrations = card(root, x + 808, y, 288, 338, 28, 'Integrations');
|
||
await sectionHeader(integrations, 22, 20, 244, 'Integrations', null);
|
||
const ints = [['Stripe', 'Connected', C.success], ['Google Calendar', 'Connected', C.success], ['Meta Ads', 'Needs auth', C.warning], ['Shopify', 'Available', C.muted]];
|
||
for (let i = 0; i < ints.length; i++) {
|
||
iconChip(integrations, 26, 78 + i * 56, i === 0 ? 'payments' : i === 1 ? 'calendar' : i === 2 ? 'marketing' : 'inventory', ints[i][2], ints[i][2], 0.12, 34);
|
||
await txt(integrations, 72, 76 + i * 56, 120, 18, ints[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(integrations, 72, 96 + i * 56, 120, 16, ints[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
}
|
||
|
||
const billing = luxuryHero(root, x + 410, y + 368, 318, 352, 'Billing');
|
||
await txt(billing, 26, 26, 120, 18, 'BILLING', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(billing, 26, 58, 190, 44, 'Luxe Growth', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(billing, 26, 112, 220, 30, '$249 / month · 3 locations included', 12, C.rose2, 'medium', 'LEFT');
|
||
await progressBar(billing, 30, 184, 248, 'Locations', 66, C.gold2);
|
||
await progressBar(billing, 30, 244, 248, 'Team seats', 72, C.gold2);
|
||
await pill(billing, 46, 300, 214, 34, 'Manage subscription', C.goldDark, C.gold2);
|
||
|
||
const controls = card(root, x + 756, y + 368, 340, 352, 28, 'Business controls');
|
||
await sectionHeader(controls, 24, 20, 292, 'Business controls', null);
|
||
await formField(controls, 24, 76, 292, 'Timezone', 'America / New York');
|
||
await formField(controls, 24, 150, 292, 'Currency', 'USD — United States Dollar');
|
||
await formField(controls, 24, 224, 292, 'Tax profile', 'Salon services 8.875%, retail 8.875%');
|
||
await pill(controls, 74, 306, 192, 32, 'Open audit log', C.surface, C.wine700);
|
||
}
|
||
|
||
async function appointmentCard(parent, x, y, w, title, client, meta, color = C.wine700) {
|
||
const c = frame(parent, x, y, w, 104, C.surface, 0.82, 22, 'Appointment card - ' + title);
|
||
c.effects = effects('soft');
|
||
addStroke(c, C.border, 0.72);
|
||
rect(c, 0, 0, 5, 104, color, 1, 4, 'Status accent');
|
||
await txt(c, 20, 16, w - 44, 20, title, 13, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(c, 20, 42, w - 44, 18, client, 11, C.text2, 'semibold', 'LEFT');
|
||
await txt(c, 20, 66, w - 44, 18, meta, 10, C.muted, 'medium', 'LEFT');
|
||
return c;
|
||
}
|
||
|
||
async function pageAppointmentsBoard(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
await metricCard(root, x, y, 252, 116, 'Upcoming today', '94', '+18%', 'calendar', C.wine700);
|
||
await metricCard(root, x + 280, y, 252, 116, 'Checked in', '18', '+9%', 'clients', C.rose);
|
||
await metricCard(root, x + 560, y, 252, 116, 'Waitlist', '11', '-3%', 'calendar', C.goldDark);
|
||
await metricCard(root, x + 840, y, 256, 116, 'Deposits held', '$7.8k', '+21%', 'payments', C.success);
|
||
|
||
const board = card(root, x, y + 144, 804, 606, 30, 'Appointments kanban board');
|
||
await sectionHeader(board, 24, 22, 756, 'Appointment status flow', 'New booking');
|
||
const lanes = [
|
||
['Waiting', C.warning, [['10:00', 'Mia Carter', 'Gloss refresh'], ['10:15', 'Sofia Reed', 'Bridal trial'], ['10:30', 'Ava Stone', 'Nail atelier']]],
|
||
['Confirmed', C.wine700, [['11:00', 'Lena Moss', 'Balayage ritual'], ['11:30', 'Isla Vale', 'Hydrafacial elite']]],
|
||
['In service', C.rose, [['12:00', 'Nora Lane', 'Color suite'], ['12:20', 'Ella King', 'Luxury facial']]],
|
||
['Checkout', C.success, [['12:45', 'Olivia Hart', 'Retail bundle'], ['13:10', 'Amara Bell', 'Deposit paid']]]
|
||
];
|
||
for (let i = 0; i < lanes.length; i++) {
|
||
const lx = 24 + i * 192;
|
||
const lane = frame(board, lx, 78, 174, 500, C.cream, 0.52, 24, 'Lane - ' + lanes[i][0]);
|
||
addStroke(lane, C.border, 0.58);
|
||
await txt(lane, 16, 16, 104, 20, lanes[i][0], 14, C.text, 'bold', 'LEFT', 'display');
|
||
await pill(lane, 118, 13, 40, 24, String(lanes[i][2].length), lanes[i][1], C.surface);
|
||
for (let j = 0; j < lanes[i][2].length; j++) {
|
||
await appointmentCard(lane, 14, 58 + j * 128, 146, lanes[i][2][j][0], lanes[i][2][j][1], lanes[i][2][j][2], lanes[i][1]);
|
||
}
|
||
}
|
||
|
||
const side = luxuryHero(root, x + 832, y + 144, 264, 606, 'VIP appointment insight');
|
||
await txt(side, 28, 28, 150, 18, 'NEXT VIP', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(side, 28, 66, 170, 48, 'Mila West', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(side, 28, 124, 188, 44, 'Color correction suite with Olivia. Prefers champagne tea and private room.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await progressBar(side, 28, 210, 202, 'Preparation', 76, C.gold2);
|
||
await pill(side, 28, 276, 132, 30, 'Deposit paid', C.goldDark, C.gold2);
|
||
await pill(side, 28, 320, 158, 30, 'Allergy note', C.surface, C.surface);
|
||
await buttonComponent(side, 28, 420, 202, 44, 'Open details', 'gold', 'default');
|
||
await buttonComponent(side, 28, 480, 202, 44, 'Message client', 'ghost', 'default');
|
||
}
|
||
|
||
async function pageAppointmentDetails(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const client = luxuryHero(root, x, y, 360, 720, 'Appointment client profile');
|
||
await avatar(client, 32, 34, 74, 'MW', C.gold, C.wine800);
|
||
await txt(client, 32, 126, 230, 42, 'Mila West', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(client, 32, 178, 230, 36, 'VIP guest · 18 visits · champagne preference', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(client, 32, 238, 114, 30, 'VIP Gold', C.goldDark, C.gold2);
|
||
await pill(client, 158, 238, 128, 30, 'Deposit paid', C.surface, C.surface);
|
||
await photoTile(client, 32, 308, 296, 190, 'Color inspiration', 'Attached consultation image', C.rose);
|
||
await txt(client, 32, 538, 250, 20, 'Client notes', 16, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(client, 32, 570, 276, 60, 'Sensitive scalp. Use low-ammonia formula and schedule a quiet private room.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
|
||
const detail = card(root, x + 388, y, 430, 720, 30, 'Appointment details panel');
|
||
await sectionHeader(detail, 28, 26, 374, 'Booking summary', 'Edit');
|
||
await formField(detail, 28, 84, 374, 'Service', 'Signature balayage ritual');
|
||
await formField(detail, 28, 164, 176, 'Date', 'Friday, June 12');
|
||
await formField(detail, 226, 164, 176, 'Time', '11:00 AM - 1:15 PM');
|
||
await formField(detail, 28, 244, 176, 'Specialist', 'Olivia Bennett');
|
||
await formField(detail, 226, 244, 176, 'Room', 'Private Suite 02');
|
||
const timeline = glass(detail, 28, 344, 374, 228, 24, 'Appointment timeline');
|
||
await txt(timeline, 24, 18, 200, 22, 'Timeline', 17, C.text, 'bold', 'LEFT', 'display');
|
||
await simpleTimeline(timeline, 26, 62, [
|
||
['Deposit captured', '$120 paid online'],
|
||
['Reminder sent', 'SMS and email delivered'],
|
||
['Consultation note', 'Formula saved by Olivia']
|
||
], C.wine700);
|
||
await buttonComponent(detail, 28, 616, 166, 44, 'Check in', 'primary', 'default');
|
||
await buttonComponent(detail, 210, 616, 126, 44, 'Reschedule', 'secondary', 'default');
|
||
|
||
const payment = card(root, x + 846, y, 250, 720, 30, 'Appointment payment sidebar');
|
||
await sectionHeader(payment, 24, 24, 202, 'Payment', null);
|
||
await labelValue(payment, 24, 80, 'Service subtotal', '$320');
|
||
await labelValue(payment, 24, 136, 'Deposit paid', '$120');
|
||
await labelValue(payment, 24, 192, 'Retail add-ons', '$86');
|
||
await labelValue(payment, 24, 248, 'Balance due', '$286', C.wine700);
|
||
barChart(payment, 32, 332, 182, 110, [40, 64, 86, 58, 96, 72], C.gold, C.border);
|
||
await txt(payment, 24, 480, 190, 42, 'Suggested retail bundle: gloss mask, silk serum, and scalp repair treatment.', 11, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await buttonComponent(payment, 24, 588, 202, 44, 'Open checkout', 'gold', 'default');
|
||
}
|
||
|
||
async function pageAddAppointmentModal(root) {
|
||
await pageAppointmentsBoard(root);
|
||
const modal = await modalShell(root, 'Add appointment', 'Create a premium booking with service, specialist, room, and deposit rules.', 760, 660);
|
||
await formField(modal, 34, 118, 330, 'Client', 'Mila West');
|
||
await formField(modal, 396, 118, 330, 'Service', 'Signature balayage ritual');
|
||
await formField(modal, 34, 198, 210, 'Date', 'Friday, June 12');
|
||
await formField(modal, 270, 198, 210, 'Start time', '11:00 AM');
|
||
await formField(modal, 506, 198, 220, 'Duration', '135 minutes');
|
||
const slots = glass(modal, 34, 302, 330, 192, 24, 'Specialist availability');
|
||
await txt(slots, 24, 20, 220, 22, 'Available specialists', 17, C.text, 'bold', 'LEFT', 'display');
|
||
const people = [['Olivia Bennett', 'Color expert', C.gold], ['Maya Cole', 'Senior stylist', C.wine700], ['Nora Field', 'Facial specialist', C.rose]];
|
||
for (let i = 0; i < people.length; i++) {
|
||
await avatar(slots, 24, 62 + i * 38, 28, people[i][0].slice(0, 2), people[i][2], C.surface);
|
||
await txt(slots, 62, 58 + i * 38, 130, 16, people[i][0], 11, C.text, 'bold', 'LEFT');
|
||
await txt(slots, 62, 76 + i * 38, 120, 14, people[i][1], 9, C.muted, 'medium', 'LEFT');
|
||
await pill(slots, 218, 62 + i * 38, 76, 24, i === 0 ? 'Best' : 'Open', i === 0 ? C.goldDark : C.wine700, i === 0 ? C.gold2 : C.roseSoft);
|
||
}
|
||
const rules = glass(modal, 396, 302, 330, 192, 24, 'Booking rules');
|
||
await txt(rules, 24, 20, 200, 22, 'Booking rules', 17, C.text, 'bold', 'LEFT', 'display');
|
||
await formField(rules, 24, 62, 282, 'Deposit policy', '40% for services above $250');
|
||
await formField(rules, 24, 130, 282, 'Reminder sequence', '48h email, 24h SMS, 2h push');
|
||
await buttonComponent(modal, 382, 556, 160, 48, 'Save booking', 'primary', 'default', 'plus');
|
||
await buttonComponent(modal, 558, 556, 120, 48, 'Cancel', 'secondary', 'default');
|
||
}
|
||
|
||
async function pageClientProfile(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const profile = luxuryHero(root, x, y, 348, 720, 'Client profile hero');
|
||
await avatar(profile, 34, 36, 82, 'AR', C.gold, C.wine800);
|
||
await txt(profile, 34, 134, 250, 42, 'Ariana Reed', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(profile, 34, 188, 250, 40, 'Loyalty Platinum · 24 visits · prefers quiet suite and rose tea.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(profile, 34, 256, 116, 30, 'Platinum', C.goldDark, C.gold2);
|
||
await pill(profile, 162, 256, 110, 30, 'No-show 0%', C.surface, C.surface);
|
||
await metricCard(profile, 34, 332, 128, 112, 'Spend', '$8.6k', '+16%', 'payments', C.gold2);
|
||
await metricCard(profile, 184, 332, 128, 112, 'Visits', '24', '+4', 'calendar', C.rose2);
|
||
await photoTile(profile, 34, 492, 278, 150, 'Style board', 'Preferred tones and looks', C.rose);
|
||
|
||
const tabs = card(root, x + 376, y, 720, 720, 30, 'Client profile workspace');
|
||
await sectionHeader(tabs, 28, 24, 664, 'Client relationship workspace', 'Book again');
|
||
const tabLabels = ['Overview', 'Visits', 'Products', 'Notes', 'Files'];
|
||
for (let i = 0; i < tabLabels.length; i++) await pill(tabs, 28 + i * 116, 76, 98, 30, tabLabels[i], i === 0 ? C.wine700 : C.muted, i === 0 ? C.roseSoft : C.cream);
|
||
const history = card(tabs, 28, 132, 330, 520, 26, 'Visit history');
|
||
await sectionHeader(history, 22, 20, 286, 'Visit history', 'View all');
|
||
await simpleTimeline(history, 26, 78, [
|
||
['Balayage refresh', '$320 · Olivia · 5 days ago'],
|
||
['Hydrafacial elite', '$180 · Nora · Apr 18'],
|
||
['Retail purchase', '$126 · Serum bundle'],
|
||
['Color consultation', 'Formula updated and saved']
|
||
], C.wine700);
|
||
const preferences = card(tabs, 384, 132, 308, 246, 26, 'Client preferences');
|
||
await sectionHeader(preferences, 22, 20, 264, 'Preferences', 'Edit');
|
||
await pill(preferences, 22, 76, 110, 30, 'Quiet room', C.wine700, C.roseSoft);
|
||
await pill(preferences, 146, 76, 110, 30, 'Rose tea', C.goldDark, C.warningSoft);
|
||
await pill(preferences, 22, 118, 138, 30, 'Low scent', C.success, C.successSoft);
|
||
await txt(preferences, 22, 168, 248, 42, 'Avoid lavender products. Likes warm caramel hair tones and weekend appointments.', 11, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
const retail = card(tabs, 384, 406, 308, 246, 26, 'Retail recommendations');
|
||
await sectionHeader(retail, 22, 20, 264, 'Recommended retail', 'Send cart');
|
||
await table(retail, 22, 72, 264, ['Product', 'Fit', 'Price'], [['Silk serum', '96%', '$48'], ['Gloss mask', '92%', '$62'], ['Scalp mist', '87%', '$34']], [104, 70, 70], 42);
|
||
}
|
||
|
||
async function pageClientMedicalRecord(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const alert = luxuryHero(root, x, y, 1096, 164, 'Secure medical record banner');
|
||
await txt(alert, 32, 30, 220, 18, 'SECURE CLIENT RECORD', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(alert, 32, 64, 440, 48, 'Beauty and wellness safety profile', 40, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(alert, 520, 54, 408, 48, 'This screen models the private record view for allergies, contraindications, treatment history, files, and signed consent.', 13, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(alert, 940, 62, 118, 32, 'Restricted', C.goldDark, C.gold2);
|
||
|
||
const allergies = card(root, x, y + 194, 330, 526, 30, 'Allergies and contraindications');
|
||
await sectionHeader(allergies, 24, 22, 282, 'Safety alerts', 'Add note');
|
||
const flags = [['Latex sensitivity', C.danger], ['Avoid retinol peel', C.warning], ['Low-scent products', C.wine700], ['Patch test required', C.goldDark]];
|
||
for (let i = 0; i < flags.length; i++) {
|
||
const row = glass(allergies, 24, 76 + i * 78, 282, 58, 18, 'Safety flag');
|
||
ellipse(row, 18, 20, 18, 18, flags[i][1], 0.9, 'Flag dot');
|
||
await txt(row, 50, 12, 190, 18, flags[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(row, 50, 32, 190, 14, i === 0 ? 'Confirmed by client' : 'Review before treatment', 9, C.muted, 'medium', 'LEFT');
|
||
}
|
||
|
||
const treatments = card(root, x + 358, y + 194, 420, 526, 30, 'Treatment history');
|
||
await sectionHeader(treatments, 24, 22, 372, 'Treatment record', 'Export');
|
||
await simpleTimeline(treatments, 28, 84, [
|
||
['Hydrafacial elite', 'No irritation. Barrier serum recommended.'],
|
||
['Chemical peel consult', 'Deferred due to retinol use.'],
|
||
['Laser patch test', 'Passed behind left ear.'],
|
||
['Color correction', 'Formula recorded and approved.']
|
||
], C.wine700);
|
||
await photoTile(treatments, 28, 374, 164, 104, 'Before', 'Private image', C.rose);
|
||
await photoTile(treatments, 208, 374, 164, 104, 'After', 'Private image', C.gold);
|
||
|
||
const consent = card(root, x + 806, y + 194, 290, 526, 30, 'Consent documents');
|
||
await sectionHeader(consent, 24, 22, 242, 'Consent', 'Request');
|
||
await progressBar(consent, 24, 86, 232, 'Forms complete', 86, C.success);
|
||
await table(consent, 24, 160, 242, ['Document', 'Status'], [['Treatment consent', 'Signed'], ['Patch test', 'Signed'], ['Photo release', 'Pending'], ['Aftercare', 'Sent']], [144, 82], 44);
|
||
await buttonComponent(consent, 24, 438, 242, 44, 'Send consent link', 'primary', 'default');
|
||
}
|
||
|
||
async function pageAddClientModal(root) {
|
||
await pageClientCRM(root);
|
||
const modal = await modalShell(root, 'Add client', 'Create a complete guest profile with contact, preferences, tags, and consent state.', 760, 650);
|
||
await formField(modal, 34, 118, 330, 'Full name', 'Ariana Reed');
|
||
await formField(modal, 396, 118, 330, 'Phone number', '+1 555 0198');
|
||
await formField(modal, 34, 198, 330, 'Email', 'ariana@example.com');
|
||
await formField(modal, 396, 198, 330, 'Birthday', 'September 14');
|
||
await formField(modal, 34, 278, 330, 'Preferred specialist', 'Olivia Bennett');
|
||
await formField(modal, 396, 278, 330, 'Preferred channel', 'SMS and email');
|
||
const prefs = glass(modal, 34, 380, 330, 126, 24, 'Client tags');
|
||
await txt(prefs, 22, 18, 190, 20, 'Tags and preferences', 15, C.text, 'bold', 'LEFT', 'display');
|
||
await pill(prefs, 22, 58, 92, 28, 'VIP lead', C.goldDark, C.gold2);
|
||
await pill(prefs, 126, 58, 92, 28, 'Color', C.wine700, C.roseSoft);
|
||
await pill(prefs, 230, 58, 76, 28, 'Quiet', C.success, C.successSoft);
|
||
const consent = glass(modal, 396, 380, 330, 126, 24, 'Client consent');
|
||
await txt(consent, 22, 18, 190, 20, 'Consent options', 15, C.text, 'bold', 'LEFT', 'display');
|
||
await pill(consent, 22, 58, 128, 28, 'Marketing opt-in', C.wine700, C.roseSoft);
|
||
await pill(consent, 166, 58, 110, 28, 'Medical form', C.warning, C.warningSoft);
|
||
await buttonComponent(modal, 396, 554, 158, 48, 'Create client', 'primary', 'default', 'plus');
|
||
await buttonComponent(modal, 570, 554, 120, 48, 'Cancel', 'secondary', 'default');
|
||
}
|
||
|
||
async function pageStaffSchedule(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const grid = card(root, x, y, 820, 720, 30, 'Staff schedule grid');
|
||
await sectionHeader(grid, 24, 22, 772, 'Weekly team schedule', 'Publish');
|
||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||
for (let i = 0; i < days.length; i++) await txt(grid, 170 + i * 98, 76, 70, 18, days[i], 11, C.muted, 'bold', 'CENTER');
|
||
const staff = ['Olivia Bennett', 'Maya Cole', 'Nora Field', 'Lena Hart', 'Sofia Reed'];
|
||
for (let r = 0; r < staff.length; r++) {
|
||
await avatar(grid, 28, 110 + r * 104, 40, staff[r].slice(0, 2), r % 2 ? C.rose : C.gold, C.wine800);
|
||
await txt(grid, 78, 108 + r * 104, 86, 18, staff[r], 11, C.text, 'bold', 'LEFT');
|
||
await txt(grid, 78, 128 + r * 104, 86, 16, r % 2 ? 'Stylist' : 'Specialist', 9, C.muted, 'medium', 'LEFT');
|
||
for (let d = 0; d < days.length; d++) {
|
||
const isOff = (r + d) % 5 === 0;
|
||
const block = frame(grid, 174 + d * 98, 104 + r * 104, 78, 72, isOff ? C.cream2 : C.roseSoft, isOff ? 0.55 : 0.9, 18, 'Shift block');
|
||
addStroke(block, isOff ? C.border : C.borderWine, 0.7);
|
||
await txt(block, 0, 14, 78, 18, isOff ? 'Off' : '9 - 6', 11, isOff ? C.muted : C.wine700, 'bold', 'CENTER');
|
||
await txt(block, 0, 38, 78, 16, isOff ? 'Leave' : 'Suite ' + ((d % 3) + 1), 9, C.muted, 'medium', 'CENTER');
|
||
}
|
||
}
|
||
|
||
const requests = card(root, x + 848, y, 248, 720, 30, 'Shift requests');
|
||
await sectionHeader(requests, 24, 22, 200, 'Requests', 'Review');
|
||
await progressBar(requests, 24, 84, 200, 'Coverage', 88, C.success);
|
||
await simpleTimeline(requests, 26, 166, [
|
||
['Maya swap request', 'Saturday 12 PM to 6 PM'],
|
||
['Nora training', 'Hydrafacial certification'],
|
||
['Olivia overtime', 'VIP color correction']
|
||
], C.wine700);
|
||
await buttonComponent(requests, 24, 608, 200, 44, 'Approve all', 'primary', 'default');
|
||
}
|
||
|
||
async function pageEmployeeProfile(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const hero = luxuryHero(root, x, y, 360, 720, 'Employee profile hero');
|
||
await avatar(hero, 34, 36, 82, 'OB', C.gold, C.wine800);
|
||
await txt(hero, 34, 132, 240, 42, 'Olivia Bennett', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(hero, 34, 186, 260, 42, 'Lead colorist · 4.96 rating · highest balayage retention.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(hero, 34, 254, 128, 30, 'Color expert', C.goldDark, C.gold2);
|
||
await pill(hero, 174, 254, 100, 30, 'Top 5%', C.surface, C.surface);
|
||
await metricCard(hero, 34, 330, 128, 112, 'Revenue', '$22.4k', '+14%', 'payments', C.gold2);
|
||
await metricCard(hero, 184, 330, 128, 112, 'Rebook', '74%', '+8%', 'calendar', C.rose2);
|
||
await photoTile(hero, 34, 498, 278, 140, 'Portfolio', 'Signature color work', C.rose);
|
||
|
||
const work = card(root, x + 388, y, 430, 720, 30, 'Employee performance');
|
||
await sectionHeader(work, 24, 24, 382, 'Performance and services', 'Edit');
|
||
await table(work, 24, 78, 382, ['Service', 'Bookings', 'Revenue'], [['Balayage', '42', '$12.8k'], ['Gloss', '31', '$4.2k'], ['Color correction', '12', '$5.4k'], ['Consultation', '26', '$1.3k']], [166, 86, 98], 48);
|
||
const chart = glass(work, 24, 330, 382, 170, 24, 'Employee revenue chart');
|
||
await txt(chart, 22, 18, 180, 20, 'Revenue trend', 15, C.text, 'bold', 'LEFT', 'display');
|
||
lineChart(chart, 30, 70, 316, 70, [48, 62, 58, 74, 92, 88, 104], C.wine700);
|
||
await buttonComponent(work, 24, 560, 160, 44, 'Message', 'primary', 'default');
|
||
await buttonComponent(work, 200, 560, 160, 44, 'View payroll', 'secondary', 'default');
|
||
|
||
const schedule = card(root, x + 846, y, 250, 720, 30, 'Employee schedule preview');
|
||
await sectionHeader(schedule, 24, 24, 202, 'This week', null);
|
||
await simpleTimeline(schedule, 26, 86, [
|
||
['Monday', '9 AM - 6 PM · Suite 01'],
|
||
['Tuesday', '10 AM - 7 PM · Suite 03'],
|
||
['Wednesday', 'Training · 2 PM'],
|
||
['Friday', 'VIP private bookings']
|
||
], C.goldDark);
|
||
await progressBar(schedule, 24, 460, 200, 'Utilization', 91, C.goldDark);
|
||
await buttonComponent(schedule, 24, 584, 202, 44, 'Adjust schedule', 'gold', 'default');
|
||
}
|
||
|
||
async function pageServiceDetails(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const hero = luxuryHero(root, x, y, 420, 720, 'Service details hero');
|
||
await txt(hero, 34, 34, 160, 18, 'SIGNATURE SERVICE', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(hero, 34, 72, 300, 90, 'Balayage Ritual', 48, C.surface, 'heavy', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(hero, 34, 182, 300, 56, 'Premium color service with consultation, bond treatment, toner, blowout, and aftercare prescription.', 13, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await photoTile(hero, 34, 276, 330, 190, 'Service imagery', 'Luxury color transformation', C.gold);
|
||
await labelValue(hero, 34, 510, 'Base price', '$320', C.surface);
|
||
await labelValue(hero, 180, 510, 'Duration', '135 min', C.surface);
|
||
await buttonComponent(hero, 34, 620, 160, 44, 'Book service', 'gold', 'default');
|
||
|
||
const config = card(root, x + 448, y, 648, 720, 30, 'Service configuration');
|
||
await sectionHeader(config, 28, 24, 592, 'Pricing, add-ons, and profitability', 'Save');
|
||
const rules = card(config, 28, 80, 292, 240, 24, 'Price rules');
|
||
await sectionHeader(rules, 22, 18, 248, 'Price rules', null);
|
||
await formField(rules, 22, 68, 248, 'Starting price', '$320');
|
||
await formField(rules, 22, 140, 248, 'Deposit required', '40% for first-time clients');
|
||
const addons = card(config, 344, 80, 276, 240, 24, 'Service add-ons');
|
||
await sectionHeader(addons, 22, 18, 232, 'Add-ons', 'Add');
|
||
await table(addons, 22, 66, 232, ['Add-on', 'Price'], [['Bond repair', '$45'], ['Gloss toner', '$38'], ['Scalp ritual', '$55']], [144, 66], 42);
|
||
const specialists = card(config, 28, 352, 292, 282, 24, 'Assigned specialists');
|
||
await sectionHeader(specialists, 22, 18, 248, 'Specialists', 'Manage');
|
||
const sp = [['Olivia', '96% satisfaction', C.gold], ['Maya', '91% satisfaction', C.wine700], ['Lena', '88% satisfaction', C.rose]];
|
||
for (let i = 0; i < sp.length; i++) {
|
||
await avatar(specialists, 22, 70 + i * 58, 36, sp[i][0].slice(0, 2), sp[i][2], C.surface);
|
||
await txt(specialists, 70, 68 + i * 58, 130, 18, sp[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(specialists, 70, 90 + i * 58, 150, 16, sp[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
}
|
||
const profit = card(config, 344, 352, 276, 282, 24, 'Profitability');
|
||
await sectionHeader(profit, 22, 18, 232, 'Profitability', null);
|
||
barChart(profit, 34, 90, 200, 110, [64, 72, 80, 96, 84, 112], C.wine700, C.border);
|
||
await progressBar(profit, 34, 224, 200, 'Margin', 68, C.success);
|
||
}
|
||
|
||
async function pageAddProductModal(root) {
|
||
await pageInventory(root);
|
||
const modal = await modalShell(root, 'Add product', 'Create a retail SKU with supplier, pricing, stock, reorder rules, and storefront visibility.', 780, 670);
|
||
await photoTile(modal, 34, 116, 220, 200, 'Product image', 'Upload product photo', C.gold);
|
||
await formField(modal, 286, 118, 220, 'Product name', 'Silk Repair Serum');
|
||
await formField(modal, 530, 118, 216, 'SKU', 'SR-SERUM-048');
|
||
await formField(modal, 286, 198, 220, 'Vendor', 'Maison Luxe Supply');
|
||
await formField(modal, 530, 198, 216, 'Category', 'Hair care');
|
||
await formField(modal, 286, 278, 220, 'Retail price', '$48.00');
|
||
await formField(modal, 530, 278, 216, 'Cost', '$19.40');
|
||
const stock = glass(modal, 34, 356, 712, 138, 24, 'Stock rules');
|
||
await txt(stock, 24, 20, 160, 20, 'Stock rules', 15, C.text, 'bold', 'LEFT', 'display');
|
||
await formField(stock, 24, 56, 200, 'Current stock', '42 units');
|
||
await formField(stock, 252, 56, 200, 'Reorder at', '12 units');
|
||
await formField(stock, 480, 56, 200, 'Store visibility', 'Featured product');
|
||
await buttonComponent(modal, 414, 562, 162, 48, 'Add product', 'primary', 'default', 'plus');
|
||
await buttonComponent(modal, 592, 562, 120, 48, 'Cancel', 'secondary', 'default');
|
||
}
|
||
|
||
async function pageOrdersDashboard(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
await metricCard(root, x, y, 252, 116, 'Open orders', '28', '+12%', 'orders', C.wine700);
|
||
await metricCard(root, x + 280, y, 252, 116, 'Vendor spend', '$18.6k', '-4%', 'payments', C.goldDark);
|
||
await metricCard(root, x + 560, y, 252, 116, 'In transit', '16', '+7%', 'inventory', C.rose);
|
||
await metricCard(root, x + 840, y, 256, 116, 'Returns', '3', '-2%', 'orders', C.success);
|
||
const pipeline = card(root, x, y + 144, 530, 606, 30, 'Order pipeline');
|
||
await sectionHeader(pipeline, 24, 22, 482, 'Order pipeline', 'New order');
|
||
const states = [['Draft', C.muted], ['Approved', C.wine700], ['In transit', C.goldDark], ['Received', C.success]];
|
||
for (let i = 0; i < states.length; i++) {
|
||
const row = glass(pipeline, 24, 78 + i * 118, 482, 88, 22, 'Order state');
|
||
iconChip(row, 18, 24, 'orders', states[i][1], states[i][1], 0.12, 40);
|
||
await txt(row, 72, 18, 160, 20, states[i][0], 14, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(row, 72, 44, 220, 18, (i + 3) + ' orders · Maison Luxe Supply', 11, C.muted, 'medium', 'LEFT');
|
||
await pill(row, 350, 28, 88, 30, '$' + (4 + i * 3) + '.2k', states[i][1], C.cream);
|
||
}
|
||
const vendors = card(root, x + 558, y + 144, 538, 288, 30, 'Vendor table');
|
||
await sectionHeader(vendors, 24, 22, 490, 'Vendor performance', 'Manage');
|
||
await table(vendors, 24, 78, 490, ['Vendor', 'Orders', 'Fill rate', 'Spend'], [['Maison Luxe', '12', '98%', '$8.4k'], ['Glow Supply', '8', '94%', '$4.2k'], ['SpaLab Pro', '6', '91%', '$3.8k'], ['Nail Atelier', '2', '89%', '$2.2k']], [162, 90, 100, 98], 42);
|
||
const receive = luxuryHero(root, x + 558, y + 462, 538, 288, 'Receiving preview');
|
||
await txt(receive, 28, 28, 170, 18, 'RECEIVING TODAY', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(receive, 28, 64, 280, 44, '3 deliveries need quality check.', 34, C.surface, 'bold', 'LEFT', 'display');
|
||
await progressBar(receive, 28, 150, 430, 'Receiving progress', 62, C.gold2);
|
||
await buttonComponent(receive, 28, 212, 176, 44, 'Start receiving', 'gold', 'default');
|
||
}
|
||
|
||
async function pageStoreDashboard(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const preview = luxuryHero(root, x, y, 384, 720, 'Storefront preview');
|
||
await txt(preview, 32, 34, 150, 18, 'ONLINE STORE', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(preview, 32, 72, 260, 88, 'Retail shelf for salon guests.', 46, C.surface, 'heavy', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(preview, 32, 182, 280, 42, 'Curated aftercare products, bundles, and campaign-driven carts.', 13, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await photoTile(preview, 32, 270, 320, 190, 'Featured shelf', 'Hair care and skin rituals', C.gold);
|
||
await metricCard(preview, 32, 512, 138, 112, 'Cart value', '$86', '+11%', 'store', C.gold2);
|
||
await metricCard(preview, 194, 512, 138, 112, 'Conversion', '8.4%', '+2%', 'analytics', C.rose2);
|
||
|
||
const shop = card(root, x + 412, y, 684, 720, 30, 'Store dashboard workspace');
|
||
await sectionHeader(shop, 28, 24, 628, 'Products, carts, and merchandising', 'Publish');
|
||
const products = [['Silk Serum', '$48', 'Best seller', C.gold], ['Gloss Mask', '$62', 'Bundle', C.rose], ['Scalp Mist', '$34', 'New', C.violet], ['Repair Oil', '$55', 'Low stock', C.warning]];
|
||
for (let i = 0; i < products.length; i++) {
|
||
const col = i % 2;
|
||
const row = Math.floor(i / 2);
|
||
const p = card(shop, 28 + col * 320, 84 + row * 190, 292, 160, 24, 'Product card');
|
||
await photoTile(p, 18, 18, 86, 100, products[i][0], products[i][2], products[i][3]);
|
||
await txt(p, 122, 26, 130, 20, products[i][0], 14, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(p, 122, 54, 110, 18, products[i][1], 12, C.wine700, 'bold', 'LEFT');
|
||
await pill(p, 122, 90, 104, 28, products[i][2], products[i][3], C.cream);
|
||
}
|
||
const carts = glass(shop, 28, 492, 628, 156, 26, 'Abandoned carts');
|
||
await sectionHeader(carts, 24, 20, 580, 'Abandoned carts', 'Recover');
|
||
await table(carts, 24, 72, 580, ['Client', 'Cart', 'Value', 'Action'], [['Mila West', '3 items', '$148', 'Send SMS'], ['Ariana Reed', '2 items', '$96', 'Email'], ['Sofia Bell', '4 items', '$212', 'Call']], [156, 120, 90, 154], 42);
|
||
}
|
||
|
||
async function pageReportsDashboard(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const filters = card(root, x, y, 1096, 132, 30, 'Report filters');
|
||
await sectionHeader(filters, 28, 24, 1040, 'Reports command center', 'Export PDF');
|
||
await pill(filters, 28, 76, 120, 30, 'This quarter', C.wine700, C.roseSoft);
|
||
await pill(filters, 162, 76, 106, 30, 'All branches', C.goldDark, C.warningSoft);
|
||
await pill(filters, 282, 76, 128, 30, 'Services + retail', C.success, C.successSoft);
|
||
await pill(filters, 424, 76, 116, 30, 'Saved view', C.violet, C.violetSoft);
|
||
|
||
const cards = [
|
||
['Executive summary', 'Revenue, bookings, retention, and top risks.', C.wine700],
|
||
['Operations report', 'Capacity, utilization, rooms, and late arrivals.', C.goldDark],
|
||
['Client report', 'Segments, loyalty, churn risk, and VIP behavior.', C.rose],
|
||
['Retail report', 'Sell-through, margins, vendors, and products.', C.violet]
|
||
];
|
||
for (let i = 0; i < cards.length; i++) {
|
||
const c = card(root, x + (i % 2) * 558, y + 164 + Math.floor(i / 2) * 230, 538, 202, 28, 'Saved report card');
|
||
iconChip(c, 24, 26, i === 0 ? 'analytics' : i === 1 ? 'calendar' : i === 2 ? 'clients' : 'store', cards[i][2], cards[i][2], 0.12, 44);
|
||
await txt(c, 86, 28, 250, 24, cards[i][0], 18, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(c, 86, 62, 340, 36, cards[i][1], 12, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await buttonComponent(c, 86, 124, 134, 40, 'Open report', 'primary', 'default');
|
||
await buttonComponent(c, 236, 124, 112, 40, 'Schedule', 'secondary', 'default');
|
||
}
|
||
const exports = card(root, x, y + 644, 1096, 106, 28, 'Recent exports');
|
||
await table(exports, 24, 24, 1048, ['Report', 'Owner', 'Format', 'Created', 'Status'], [['May Executive Summary', 'Emma Lane', 'PDF', 'Today', 'Ready']], [260, 180, 140, 180, 180], 42);
|
||
}
|
||
|
||
async function pageRevenueAnalytics(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
await metricCard(root, x, y, 252, 116, 'Net revenue', '$184.2k', '+22%', 'payments', C.wine700);
|
||
await metricCard(root, x + 280, y, 252, 116, 'Average ticket', '$186', '+9%', 'analytics', C.goldDark);
|
||
await metricCard(root, x + 560, y, 252, 116, 'Retail attach', '38%', '+6%', 'store', C.rose);
|
||
await metricCard(root, x + 840, y, 256, 116, 'Forecast', '$212k', '+15%', 'analytics', C.success);
|
||
const trend = card(root, x, y + 148, 690, 384, 30, 'Revenue trend chart');
|
||
await sectionHeader(trend, 28, 24, 634, 'Revenue trend and forecast', 'Deep dive');
|
||
lineChart(trend, 46, 120, 590, 170, [82, 88, 91, 116, 108, 132, 146, 154, 172, 184], C.wine700);
|
||
await pill(trend, 46, 324, 132, 30, 'Services 72%', C.wine700, C.roseSoft);
|
||
await pill(trend, 192, 324, 118, 30, 'Retail 18%', C.goldDark, C.warningSoft);
|
||
await pill(trend, 324, 324, 132, 30, 'Membership 10%', C.success, C.successSoft);
|
||
const mix = card(root, x + 718, y + 148, 378, 384, 30, 'Revenue mix');
|
||
await sectionHeader(mix, 24, 24, 330, 'Channel mix', null);
|
||
barChart(mix, 42, 108, 286, 136, [72, 18, 10, 24, 36], C.gold, C.border);
|
||
await progressBar(mix, 42, 290, 286, 'Forecast confidence', 82, C.success);
|
||
const cohort = card(root, x, y + 562, 1096, 188, 30, 'Revenue cohort table');
|
||
await sectionHeader(cohort, 24, 20, 1048, 'Revenue cohorts', 'Export');
|
||
await table(cohort, 24, 72, 1048, ['Cohort', 'Clients', 'Revenue', 'Avg ticket', 'Retention', 'Growth'], [['VIP color', '386', '$72.4k', '$242', '78%', '+18%'], ['Skin rituals', '244', '$44.8k', '$184', '66%', '+12%'], ['Retail-only', '520', '$22.6k', '$86', '34%', '+9%']], [200, 140, 160, 160, 160, 160], 42);
|
||
}
|
||
|
||
async function pageLoyaltyProgram(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const hero = luxuryHero(root, x, y, 420, 720, 'Loyalty program hero');
|
||
await txt(hero, 34, 34, 160, 18, 'LOYALTY SUITE', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(hero, 34, 72, 300, 88, 'Rewards that feel like luxury.', 46, C.surface, 'heavy', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(hero, 34, 182, 300, 52, 'Points, tiers, referrals, birthday rituals, and VIP access built into the salon experience.', 13, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await progressBar(hero, 34, 286, 330, 'Member activation', 74, C.gold2);
|
||
await metricCard(hero, 34, 360, 140, 112, 'Members', '4.8k', '+19%', 'clients', C.gold2);
|
||
await metricCard(hero, 198, 360, 140, 112, 'Redeemed', '$9.4k', '+11%', 'payments', C.rose2);
|
||
await buttonComponent(hero, 34, 576, 176, 44, 'Create reward', 'gold', 'default');
|
||
|
||
const tiers = card(root, x + 448, y, 648, 320, 30, 'Loyalty tiers');
|
||
await sectionHeader(tiers, 28, 24, 592, 'Tier design', 'Edit tiers');
|
||
const tierData = [['Rose', '0 - 999 pts', C.rose], ['Gold', '1,000 - 2,999 pts', C.goldDark], ['Platinum', '3,000+ pts', C.wine700]];
|
||
for (let i = 0; i < tierData.length; i++) {
|
||
const t = glass(tiers, 28 + i * 204, 84, 180, 178, 26, 'Loyalty tier');
|
||
ellipse(t, 24, 24, 42, 42, tierData[i][2], 0.24, 'Tier glow');
|
||
await txt(t, 24, 84, 120, 26, tierData[i][0], 22, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(t, 24, 120, 130, 18, tierData[i][1], 11, C.muted, 'medium', 'LEFT');
|
||
}
|
||
const rewards = card(root, x + 448, y + 348, 648, 372, 30, 'Rewards builder');
|
||
await sectionHeader(rewards, 28, 24, 592, 'Rewards and referrals', 'Launch');
|
||
await table(rewards, 28, 84, 592, ['Reward', 'Trigger', 'Cost', 'Status'], [['Birthday glow ritual', 'Birthday month', '500 pts', 'Live'], ['Retail bundle credit', 'Spend $300', '800 pts', 'Draft'], ['Friend referral', 'New client', '$25 credit', 'Live'], ['VIP room upgrade', 'Platinum tier', '1,200 pts', 'Live']], [210, 160, 100, 90], 46);
|
||
await buttonComponent(rewards, 28, 300, 176, 44, 'Add reward', 'primary', 'default', 'plus');
|
||
}
|
||
|
||
async function pageNotificationCenter(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const filters = card(root, x, y, 260, 720, 30, 'Notification filters');
|
||
await sectionHeader(filters, 24, 24, 212, 'Notification center', null);
|
||
const filterItems = [['All', '32'], ['Bookings', '12'], ['Payments', '7'], ['Inventory', '5'], ['Reviews', '4'], ['System', '4']];
|
||
for (let i = 0; i < filterItems.length; i++) {
|
||
const row = frame(filters, 24, 82 + i * 62, 212, 44, i === 0 ? C.roseSoft : C.cream, 0.78, 16, 'Notification filter');
|
||
addStroke(row, i === 0 ? C.wine700 : C.border, i === 0 ? 0.34 : 0.6);
|
||
await txt(row, 16, 0, 110, 44, filterItems[i][0], 12, i === 0 ? C.wine700 : C.text2, 'bold', 'LEFT');
|
||
await pill(row, 154, 9, 40, 26, filterItems[i][1], i === 0 ? C.wine700 : C.muted, C.surface);
|
||
}
|
||
const list = card(root, x + 288, y, 520, 720, 30, 'Notification list');
|
||
await sectionHeader(list, 24, 24, 472, 'Latest alerts from the bell icon', 'Mark read');
|
||
const alerts = [
|
||
['VIP booking request', 'Mila West requested private suite tomorrow.', C.goldDark, 'Bookings'],
|
||
['Deposit failed', 'Card authentication is required for booking #2818.', C.danger, 'Payments'],
|
||
['Low stock alert', 'Silk Serum reached reorder threshold.', C.warning, 'Inventory'],
|
||
['New five-star review', 'Ariana praised Olivia for color service.', C.success, 'Reviews'],
|
||
['Staff schedule change', 'Maya requested Saturday shift swap.', C.wine700, 'Staff']
|
||
];
|
||
for (let i = 0; i < alerts.length; i++) {
|
||
const row = glass(list, 24, 82 + i * 116, 472, 92, 24, 'Notification item');
|
||
iconChip(row, 20, 24, i === 1 ? 'payments' : i === 2 ? 'inventory' : i === 3 ? 'reviews' : 'notifications', alerts[i][2], alerts[i][2], 0.12, 42);
|
||
await txt(row, 78, 18, 230, 20, alerts[i][0], 14, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(row, 78, 44, 276, 18, alerts[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
await pill(row, 368, 28, 76, 28, alerts[i][3], alerts[i][2], C.cream);
|
||
}
|
||
const detail = luxuryHero(root, x + 836, y, 260, 720, 'Notification detail panel');
|
||
await txt(detail, 28, 28, 160, 18, 'SELECTED ALERT', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(detail, 28, 66, 180, 56, 'VIP booking request', 34, C.surface, 'bold', 'LEFT', 'display', 1, 'TOP');
|
||
await txt(detail, 28, 150, 190, 70, 'This is the designed web interface shown when the notification bell is opened.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await buttonComponent(detail, 28, 282, 202, 44, 'Open appointment', 'gold', 'default');
|
||
await buttonComponent(detail, 28, 342, 202, 44, 'Message client', 'ghost', 'default');
|
||
await buttonComponent(detail, 28, 402, 202, 44, 'Dismiss alert', 'secondary', 'default');
|
||
}
|
||
|
||
async function pageMessagesChat(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const inbox = card(root, x, y, 300, 720, 30, 'Conversation inbox');
|
||
await sectionHeader(inbox, 24, 24, 252, 'Messages', 'New');
|
||
const conversations = [['Mila West', 'Can I add gloss toner?', '2m'], ['Ariana Reed', 'Thank you for today', '18m'], ['Sofia Bell', 'Need to reschedule', '1h'], ['Olivia Bennett', 'Formula note saved', '3h']];
|
||
for (let i = 0; i < conversations.length; i++) {
|
||
const row = frame(inbox, 24, 84 + i * 96, 252, 74, i === 0 ? C.roseSoft : C.cream, 0.8, 22, 'Conversation row');
|
||
addStroke(row, i === 0 ? C.wine700 : C.border, i === 0 ? 0.34 : 0.55);
|
||
await avatar(row, 16, 16, 40, conversations[i][0].slice(0, 2), i === 0 ? C.gold : C.rose, C.wine800);
|
||
await txt(row, 68, 14, 120, 18, conversations[i][0], 12, C.text, 'bold', 'LEFT');
|
||
await txt(row, 68, 36, 128, 16, conversations[i][1], 10, C.muted, 'medium', 'LEFT');
|
||
await txt(row, 204, 14, 28, 16, conversations[i][2], 9, C.muted, 'bold', 'RIGHT');
|
||
}
|
||
const chat = card(root, x + 328, y, 508, 720, 30, 'Chat thread');
|
||
await sectionHeader(chat, 24, 24, 460, 'Mila West', 'Book');
|
||
const bubbles = [
|
||
['client', 'Hi, can I add a gloss toner to tomorrow appointment?'],
|
||
['team', 'Absolutely. Olivia has a 20-minute buffer after your balayage ritual.'],
|
||
['client', 'Perfect. Please keep the private room if possible.'],
|
||
['team', 'Confirmed. Private Suite 02 and champagne tea are saved.']
|
||
];
|
||
for (let i = 0; i < bubbles.length; i++) {
|
||
const isTeam = bubbles[i][0] === 'team';
|
||
const bx = isTeam ? 150 : 24;
|
||
const bubble = frame(chat, bx, 92 + i * 112, 334, 74, isTeam ? C.wine700 : C.cream, isTeam ? 1 : 0.86, 24, 'Chat bubble');
|
||
addStroke(bubble, isTeam ? C.wine700 : C.border, 0.6);
|
||
await txt(bubble, 20, 16, 292, 42, bubbles[i][1], 12, isTeam ? C.surface : C.text2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
}
|
||
const composer = frame(chat, 24, 632, 460, 58, C.cream, 0.86, 20, 'Message composer');
|
||
addStroke(composer, C.border, 0.72);
|
||
await txt(composer, 20, 0, 240, 58, 'Type a premium response...', 12, C.muted, 'regular', 'LEFT');
|
||
await buttonComponent(composer, 346, 9, 96, 40, 'Send', 'primary', 'default');
|
||
const profile = luxuryHero(root, x + 864, y, 232, 720, 'Chat client context');
|
||
await avatar(profile, 28, 34, 64, 'MW', C.gold, C.wine800);
|
||
await txt(profile, 28, 118, 160, 30, 'Mila West', 26, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(profile, 28, 158, 170, 42, 'VIP Gold guest with active appointment tomorrow.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await progressBar(profile, 28, 246, 176, 'Loyalty', 82, C.gold2);
|
||
await buttonComponent(profile, 28, 346, 176, 44, 'Open profile', 'gold', 'default');
|
||
}
|
||
|
||
async function pageSubscriptionPlans(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const usage = luxuryHero(root, x, y, 340, 720, 'Subscription usage');
|
||
await txt(usage, 32, 32, 160, 18, 'CURRENT PLAN', 10, C.gold2, 'bold', 'LEFT');
|
||
await txt(usage, 32, 70, 220, 48, 'Luxe Growth', 40, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(usage, 32, 132, 240, 42, '$249 per month · 3 locations · 25 team seats.', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await progressBar(usage, 32, 230, 260, 'Locations used', 66, C.gold2);
|
||
await progressBar(usage, 32, 300, 260, 'Team seats used', 72, C.gold2);
|
||
await progressBar(usage, 32, 370, 260, 'Automation credits', 58, C.gold2);
|
||
await buttonComponent(usage, 32, 520, 180, 44, 'Manage billing', 'gold', 'default');
|
||
|
||
const plans = card(root, x + 368, y, 728, 430, 30, 'Plan comparison');
|
||
await sectionHeader(plans, 28, 24, 672, 'Plan comparison', 'Upgrade');
|
||
const planData = [['Starter', '$99', 'Single salon', C.rose], ['Luxe Growth', '$249', 'Multi-location suite', C.wine700], ['Enterprise', 'Custom', 'Beauty group scale', C.goldDark]];
|
||
for (let i = 0; i < planData.length; i++) {
|
||
const pc = glass(plans, 28 + i * 226, 90, 204, 280, 28, 'Subscription plan card');
|
||
addStroke(pc, i === 1 ? C.wine700 : C.surface, i === 1 ? 0.45 : 0.42, i === 1 ? 2 : 1);
|
||
await txt(pc, 24, 24, 130, 24, planData[i][0], 19, C.text, 'bold', 'LEFT', 'display');
|
||
await txt(pc, 24, 64, 130, 34, planData[i][1], 30, planData[i][3], 'bold', 'LEFT', 'display');
|
||
await txt(pc, 24, 112, 140, 36, planData[i][2], 11, C.muted, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await buttonComponent(pc, 24, 204, 140, 40, i === 1 ? 'Current' : 'Choose', i === 1 ? 'secondary' : 'primary', 'default');
|
||
}
|
||
const invoices = card(root, x + 368, y + 460, 728, 260, 30, 'Billing history');
|
||
await sectionHeader(invoices, 28, 22, 672, 'Billing history', 'Download');
|
||
await table(invoices, 28, 76, 672, ['Invoice', 'Date', 'Amount', 'Status'], [['INV-2026-05', 'May 1', '$249', 'Paid'], ['INV-2026-04', 'Apr 1', '$249', 'Paid'], ['INV-2026-03', 'Mar 1', '$249', 'Paid']], [210, 150, 120, 130], 44);
|
||
}
|
||
|
||
async function pageUserProfile(root) {
|
||
const x = CONTENT_X;
|
||
const y = 124;
|
||
const profile = luxuryHero(root, x, y, 360, 720, 'User profile hero');
|
||
await avatar(profile, 34, 36, 84, 'EL', C.gold, C.wine800);
|
||
await txt(profile, 34, 138, 230, 42, 'Emma Lane', 38, C.surface, 'bold', 'LEFT', 'display');
|
||
await txt(profile, 34, 192, 260, 40, 'Owner · full access · Luxe Beauty House', 12, C.rose2, 'medium', 'LEFT', 'text', 1, 'TOP');
|
||
await pill(profile, 34, 260, 118, 30, 'Owner role', C.goldDark, C.gold2);
|
||
await pill(profile, 164, 260, 108, 30, 'MFA on', C.surface, C.surface);
|
||
await progressBar(profile, 34, 356, 290, 'Profile strength', 92, C.gold2);
|
||
await buttonComponent(profile, 34, 512, 176, 44, 'Edit profile', 'gold', 'default');
|
||
|
||
const settings = card(root, x + 388, y, 708, 720, 30, 'User profile settings');
|
||
await sectionHeader(settings, 28, 24, 652, 'Personal profile and security', 'Save');
|
||
await formField(settings, 28, 86, 310, 'Display name', 'Emma Lane');
|
||
await formField(settings, 370, 86, 310, 'Email', 'emma@luxebeauty.com');
|
||
await formField(settings, 28, 166, 310, 'Phone', '+1 555 0144');
|
||
await formField(settings, 370, 166, 310, 'Role', 'Owner');
|
||
const security = card(settings, 28, 280, 310, 260, 26, 'Security settings');
|
||
await sectionHeader(security, 22, 18, 266, 'Security', 'Manage');
|
||
await progressBar(security, 22, 78, 250, 'Session safety', 94, C.success);
|
||
await pill(security, 22, 150, 112, 30, 'MFA enabled', C.success, C.successSoft);
|
||
await pill(security, 148, 150, 102, 30, 'SSO ready', C.wine700, C.roseSoft);
|
||
const activity = card(settings, 370, 280, 310, 260, 26, 'User activity');
|
||
await sectionHeader(activity, 22, 18, 266, 'Recent activity', null);
|
||
await simpleTimeline(activity, 24, 74, [['Signed in', 'Today · New York'], ['Approved refund', 'Yesterday · Payments'], ['Updated services', 'May 24 · Catalog']], C.wine700);
|
||
await buttonComponent(settings, 28, 608, 150, 44, 'Save profile', 'primary', 'default');
|
||
await buttonComponent(settings, 194, 608, 128, 44, 'Cancel', 'secondary', 'default');
|
||
}
|
||
|
||
async function pageEditProfileModal(root) {
|
||
await pageUserProfile(root);
|
||
const modal = await modalShell(root, 'Edit profile', 'Update personal details, avatar, preferences, and security contact information.', 740, 650);
|
||
await avatar(modal, 34, 118, 92, 'EL', C.gold, C.wine800);
|
||
await buttonComponent(modal, 150, 140, 150, 42, 'Change avatar', 'secondary', 'default');
|
||
await formField(modal, 34, 246, 318, 'Display name', 'Emma Lane');
|
||
await formField(modal, 386, 246, 318, 'Email', 'emma@luxebeauty.com');
|
||
await formField(modal, 34, 326, 318, 'Phone', '+1 555 0144');
|
||
await formField(modal, 386, 326, 318, 'Timezone', 'America / New York');
|
||
const prefs = glass(modal, 34, 428, 670, 88, 24, 'Profile preferences');
|
||
await txt(prefs, 22, 18, 180, 20, 'Preferences', 15, C.text, 'bold', 'LEFT', 'display');
|
||
await pill(prefs, 22, 50, 112, 28, 'Email alerts', C.wine700, C.roseSoft);
|
||
await pill(prefs, 148, 50, 108, 28, 'SMS urgent', C.goldDark, C.warningSoft);
|
||
await pill(prefs, 270, 50, 126, 28, 'Weekly digest', C.success, C.successSoft);
|
||
await buttonComponent(modal, 390, 554, 154, 48, 'Save changes', 'primary', 'default');
|
||
await buttonComponent(modal, 560, 554, 110, 48, 'Cancel', 'secondary', 'default');
|
||
}
|