@mounaji_npm/saas-template 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * @mounaji/saas-template — Pre-built Module Manifests
3
+ *
4
+ * These mirror the NAV_SECTIONS from mounaji-platform's sidebar.js.
5
+ * Import the ones you need and pass them to <AppShell modules={[...]} />.
6
+ *
7
+ * Icons are string emojis by default (no external icon lib required).
8
+ * Override the `icon` field with any React element or lucide-react icon.
9
+ *
10
+ * Each manifest follows the shape:
11
+ * {
12
+ * id: string — unique key
13
+ * label: string — sidebar label
14
+ * icon: string|node — emoji string or React node
15
+ * path: string — route href
16
+ * section: string|null — sidebar group (null = top, no header)
17
+ * order: number — position in section
18
+ * children: Manifest[] — sub-nav (optional)
19
+ * badge: string|null — badge label
20
+ * disabled: boolean
21
+ * hidden: boolean
22
+ * external: boolean
23
+ * }
24
+ */
25
+
26
+ // ── Top section (no group label) ─────────────────────────────────────────────
27
+
28
+ export const HOME_MODULE = {
29
+ id: 'home',
30
+ label: 'Home',
31
+ icon: '⌂',
32
+ path: '/',
33
+ section: null,
34
+ order: 0,
35
+ };
36
+
37
+ export const DASHBOARD_MODULE = {
38
+ id: 'dashboard',
39
+ label: 'Dashboard',
40
+ icon: '▦',
41
+ path: '/dashboard',
42
+ section: null,
43
+ order: 1,
44
+ children: [
45
+ { id: 'dashboard-overview', label: 'Overview', icon: '▦', path: '/dashboard', order: 0 },
46
+ { id: 'dashboard-team', label: 'Team', icon: '👥', path: '/dashboard/team', order: 1 },
47
+ { id: 'dashboard-billing', label: 'Billing', icon: '💳', path: '/dashboard/billing', order: 2 },
48
+ { id: 'dashboard-usage', label: 'Usage', icon: '📈', path: '/dashboard/usage', order: 3 },
49
+ ],
50
+ };
51
+
52
+ export const CHAT_MODULE = {
53
+ id: 'chat',
54
+ label: 'Chat',
55
+ icon: '💬',
56
+ path: '/chat',
57
+ section: null,
58
+ order: 3,
59
+ };
60
+
61
+ export const DISCOVER_MODULE = {
62
+ id: 'discover',
63
+ label: 'Discover',
64
+ icon: '🧭',
65
+ path: '/discover',
66
+ section: null,
67
+ order: 2,
68
+ badge: 'Soon',
69
+ disabled: true,
70
+ };
71
+
72
+ // ── Workspace ─────────────────────────────────────────────────────────────────
73
+
74
+ export const ASSISTANTS_MODULE = {
75
+ id: 'assistants',
76
+ label: 'Assistants',
77
+ icon: '🤖',
78
+ path: '/assistants',
79
+ section: 'Workspace',
80
+ order: 1,
81
+ };
82
+
83
+ export const KNOWLEDGE_BASE_MODULE = {
84
+ id: 'knowledge-base',
85
+ label: 'Knowledge Base',
86
+ icon: '🧠',
87
+ path: '/knowledge-base',
88
+ section: 'Workspace',
89
+ order: 2,
90
+ };
91
+
92
+ export const AGENT_WORKSPACE_MODULE = {
93
+ id: 'agent-workspace',
94
+ label: 'Agent Workspace',
95
+ icon: '⊞',
96
+ path: '/workspace',
97
+ section: 'Workspace',
98
+ order: 3,
99
+ };
100
+
101
+ export const INVENTORY_MODULE = {
102
+ id: 'inventory',
103
+ label: 'Inventory',
104
+ icon: '📦',
105
+ path: '/inventory',
106
+ section: 'Workspace',
107
+ order: 4,
108
+ };
109
+
110
+ // ── Channels ──────────────────────────────────────────────────────────────────
111
+
112
+ export const PLATFORMS_MODULE = {
113
+ id: 'platforms',
114
+ label: 'Platforms',
115
+ icon: '⚡',
116
+ path: '/platforms',
117
+ section: 'Channels',
118
+ order: 1,
119
+ children: [
120
+ { id: 'platforms-all', label: 'All Platforms', icon: '⚡', path: '/platforms', order: 0 },
121
+ { id: 'platforms-analytics', label: 'Analytics', icon: '📊', path: '/platforms/analytics', order: 1 },
122
+ ],
123
+ };
124
+
125
+ export const WORKFLOWS_MODULE = {
126
+ id: 'workflows',
127
+ label: 'Workflows',
128
+ icon: '⎇',
129
+ path: '/workflows',
130
+ section: 'Channels',
131
+ order: 2,
132
+ };
133
+
134
+ export const TASKS_MODULE = {
135
+ id: 'tasks',
136
+ label: 'Tasks',
137
+ icon: '☑',
138
+ path: '/tasks',
139
+ section: 'Channels',
140
+ order: 3,
141
+ };
142
+
143
+ export const CONNECTIONS_MODULE = {
144
+ id: 'connections',
145
+ label: 'Connections',
146
+ icon: '⛓',
147
+ path: '/connections',
148
+ section: 'Channels',
149
+ order: 4,
150
+ };
151
+
152
+ // ── Communication ─────────────────────────────────────────────────────────────
153
+
154
+ export const EMAIL_MODULE = {
155
+ id: 'email',
156
+ label: 'Email',
157
+ icon: '✉',
158
+ path: '/email',
159
+ section: 'Communication',
160
+ order: 1,
161
+ };
162
+
163
+ export const CONTACTS_MODULE = {
164
+ id: 'contacts',
165
+ label: 'Contacts',
166
+ icon: '👤',
167
+ path: '/contacts',
168
+ section: 'Communication',
169
+ order: 2,
170
+ };
171
+
172
+ // ── Account ───────────────────────────────────────────────────────────────────
173
+
174
+ export const PRICING_MODULE = {
175
+ id: 'pricing',
176
+ label: 'Pricing',
177
+ icon: '💲',
178
+ path: '/pricing',
179
+ section: 'Account',
180
+ order: 1,
181
+ };
182
+
183
+ export const API_KEYS_MODULE = {
184
+ id: 'api-keys',
185
+ label: 'API Keys',
186
+ icon: '🔑',
187
+ path: '/dashboard/api-keys',
188
+ section: 'Account',
189
+ order: 2,
190
+ };
191
+
192
+ // ── Bottom nav (Settings / Help) ──────────────────────────────────────────────
193
+
194
+ export const SETTINGS_MODULE = {
195
+ id: 'settings',
196
+ label: 'Settings',
197
+ icon: '⚙',
198
+ path: '/settings',
199
+ order: 1,
200
+ children: [
201
+ { id: 'settings-design', label: 'Design Tokens', icon: '🎨', path: '/settings/design', order: 0 },
202
+ { id: 'settings-models', label: 'AI Models', icon: '🤖', path: '/settings/models', order: 1 },
203
+ ],
204
+ };
205
+
206
+ export const HELP_MODULE = {
207
+ id: 'help',
208
+ label: 'Help',
209
+ icon: '?',
210
+ path: '/help',
211
+ order: 2,
212
+ };
213
+
214
+ // ── Preset bundles ────────────────────────────────────────────────────────────
215
+
216
+ /** The full platform nav — mirrors mounaji-platform exactly */
217
+ export const ALL_MODULES = [
218
+ HOME_MODULE,
219
+ DASHBOARD_MODULE,
220
+ DISCOVER_MODULE,
221
+ CHAT_MODULE,
222
+ ASSISTANTS_MODULE,
223
+ KNOWLEDGE_BASE_MODULE,
224
+ AGENT_WORKSPACE_MODULE,
225
+ INVENTORY_MODULE,
226
+ PLATFORMS_MODULE,
227
+ WORKFLOWS_MODULE,
228
+ TASKS_MODULE,
229
+ CONNECTIONS_MODULE,
230
+ EMAIL_MODULE,
231
+ CONTACTS_MODULE,
232
+ PRICING_MODULE,
233
+ API_KEYS_MODULE,
234
+ ];
235
+
236
+ /** Minimal starter — just the core 4 modules */
237
+ export const CORE_MODULES = [
238
+ HOME_MODULE,
239
+ DASHBOARD_MODULE,
240
+ CHAT_MODULE,
241
+ ASSISTANTS_MODULE,
242
+ KNOWLEDGE_BASE_MODULE,
243
+ ];
244
+
245
+ /** AI-focused bundle */
246
+ export const AI_BUNDLE = [
247
+ HOME_MODULE,
248
+ DASHBOARD_MODULE,
249
+ CHAT_MODULE,
250
+ ASSISTANTS_MODULE,
251
+ KNOWLEDGE_BASE_MODULE,
252
+ WORKFLOWS_MODULE,
253
+ ];
254
+
255
+ /** Operations / team bundle */
256
+ export const OPS_BUNDLE = [
257
+ HOME_MODULE,
258
+ DASHBOARD_MODULE,
259
+ TASKS_MODULE,
260
+ INVENTORY_MODULE,
261
+ CONTACTS_MODULE,
262
+ EMAIL_MODULE,
263
+ ];
@@ -0,0 +1,140 @@
1
+ /**
2
+ * DashboardPage — @mounaji/saas-template
3
+ *
4
+ * Pre-built dashboard page shell. Uses @mounaji/dashboard components.
5
+ * Compose with your own data by passing metric/activity props.
6
+ *
7
+ * Props:
8
+ * metrics — MetricCard[] props array (see @mounaji/dashboard)
9
+ * activity — ActivityFeed items array
10
+ * usageBars — UsageBar props array
11
+ * header — React node (override page header)
12
+ * tokens — Partial<DEFAULT_TOKENS>
13
+ */
14
+
15
+ import { DASHBOARD_MODULE } from '../manifests.js';
16
+ import { TOKEN_CSS_MAP } from '@mounaji_npm/tokens';
17
+
18
+ function buildInlineTokens(tokens) {
19
+ if (!tokens) return {};
20
+ const s = {};
21
+ Object.entries(tokens).forEach(([k, v]) => { const c = TOKEN_CSS_MAP[k]; if (c) s[c] = v; });
22
+ return s;
23
+ }
24
+
25
+ export { DASHBOARD_MODULE as manifest };
26
+
27
+ const DEFAULT_METRICS = [
28
+ { label: 'Total Messages', value: '—', icon: '💬', variant: 'primary' },
29
+ { label: 'Active Assistants', value: '—', icon: '🤖', variant: 'success' },
30
+ { label: 'Knowledge Docs', value: '—', icon: '🧠', variant: 'accent' },
31
+ { label: 'API Calls (30d)', value: '—', icon: '⚡', variant: 'warning' },
32
+ ];
33
+
34
+ export function DashboardPage({ metrics = DEFAULT_METRICS, activity = [], usageBars = [], header, tokens, style }) {
35
+ return (
36
+ <div style={{
37
+ padding: 'var(--mn-spacing-xl, 32px)',
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ gap: 'var(--mn-spacing-xl, 32px)',
41
+ fontFamily: 'var(--mn-font-family, inherit)',
42
+ ...buildInlineTokens(tokens),
43
+ ...style,
44
+ }}>
45
+ {/* Header */}
46
+ {header ?? (
47
+ <div>
48
+ <h1 style={{ margin: 0, fontSize: 'var(--mn-font-size-2xl, 1.5rem)', fontWeight: 700, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>
49
+ Dashboard
50
+ </h1>
51
+ <p style={{ margin: '4px 0 0', fontSize: 'var(--mn-font-size-sm, 0.875rem)', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>
52
+ Overview of your workspace activity
53
+ </p>
54
+ </div>
55
+ )}
56
+
57
+ {/* Metrics grid */}
58
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 16 }}>
59
+ {metrics.map((m, i) => (
60
+ <MetricCardShell key={m.label ?? i} {...m} />
61
+ ))}
62
+ </div>
63
+
64
+ {/* Usage + Activity row */}
65
+ <div style={{ display: 'grid', gridTemplateColumns: usageBars.length ? '1fr 1fr' : '1fr', gap: 24 }}>
66
+ {activity.length > 0 && (
67
+ <SectionCard title="Recent Activity">
68
+ {activity.map((item, i) => (
69
+ <ActivityItem key={i} item={item} />
70
+ ))}
71
+ </SectionCard>
72
+ )}
73
+ {usageBars.length > 0 && (
74
+ <SectionCard title="Plan Usage">
75
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
76
+ {usageBars.map((bar, i) => (
77
+ <UsageBarShell key={i} {...bar} />
78
+ ))}
79
+ </div>
80
+ </SectionCard>
81
+ )}
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ // Minimal inline implementations so DashboardPage has no hard dep on @mounaji/dashboard
88
+ function MetricCardShell({ label, value, icon, variant = 'primary' }) {
89
+ const colors = { primary: '#3B82F6', success: '#10B981', warning: '#F59E0B', danger: '#EF4444', accent: '#8B5CF6' };
90
+ const c = colors[variant] ?? colors.primary;
91
+ return (
92
+ <div style={{ padding: 20, borderRadius: 'var(--mn-radius-lg, 0.75rem)', backgroundColor: 'var(--mn-color-card-dark, #0B0F23)', border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))' }}>
93
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
94
+ <span style={{ fontSize: '0.8125rem', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>{label}</span>
95
+ <div style={{ width: 32, height: 32, borderRadius: 'var(--mn-radius-md, 0.5rem)', backgroundColor: `${c}18`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16 }}>{icon}</div>
96
+ </div>
97
+ <p style={{ margin: '12px 0 0', fontSize: '1.5rem', fontWeight: 700, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>{value}</p>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ function SectionCard({ title, children }) {
103
+ return (
104
+ <div style={{ borderRadius: 'var(--mn-radius-lg, 0.75rem)', backgroundColor: 'var(--mn-color-card-dark, #0B0F23)', border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))', overflow: 'hidden' }}>
105
+ <div style={{ padding: '14px 20px', borderBottom: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))' }}>
106
+ <h2 style={{ margin: 0, fontSize: '0.875rem', fontWeight: 600, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>{title}</h2>
107
+ </div>
108
+ <div style={{ padding: '0 20px' }}>{children}</div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function ActivityItem({ item }) {
114
+ return (
115
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '12px 0', borderBottom: '1px solid var(--mn-border-dark, rgba(255,255,255,0.05))' }}>
116
+ {item.icon && <span style={{ fontSize: 16, marginTop: 1 }}>{item.icon}</span>}
117
+ <div style={{ flex: 1 }}>
118
+ <p style={{ margin: 0, fontSize: '0.8125rem', color: 'var(--mn-text-primary-dark, #F0F4FF)', fontWeight: 500 }}>{item.title}</p>
119
+ {item.description && <p style={{ margin: '2px 0 0', fontSize: '0.75rem', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>{item.description}</p>}
120
+ </div>
121
+ {item.ts && <span style={{ fontSize: 11, color: 'var(--mn-text-muted-dark, #64748B)', flexShrink: 0 }}>{item.ts}</span>}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function UsageBarShell({ label, used, total, unit = '' }) {
127
+ const pct = total > 0 ? Math.round((used / total) * 100) : 0;
128
+ const color = pct >= 90 ? '#EF4444' : pct >= 70 ? '#F59E0B' : '#3B82F6';
129
+ return (
130
+ <div>
131
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
132
+ <span style={{ fontSize: '0.8125rem', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>{label}</span>
133
+ <span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>{used.toLocaleString()}{unit} / {total === Infinity ? '∞' : total.toLocaleString()}{unit}</span>
134
+ </div>
135
+ <div style={{ height: 6, borderRadius: 9999, backgroundColor: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
136
+ <div style={{ height: '100%', width: `${pct}%`, backgroundColor: color, borderRadius: 'inherit', transition: 'width 300ms' }} />
137
+ </div>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * SettingsPage — @mounaji/saas-template
3
+ *
4
+ * Pre-built settings page with tab navigation.
5
+ * The "Design" tab renders the full TokenEditor from @mounaji/admin-controls.
6
+ *
7
+ * Props:
8
+ * tabs — Tab[] — additional tabs beyond built-ins
9
+ * [{ id, label, content: ReactNode }]
10
+ * defaultTab — string (default: 'design')
11
+ * tokens — Partial<DEFAULT_TOKENS>
12
+ */
13
+
14
+ import { useState } from 'react';
15
+ import { useTokens, TOKEN_GROUPS, DEFAULT_TOKENS, tokensToCSS } from '@mounaji_npm/tokens';
16
+ import { SETTINGS_MODULE } from '../manifests.js';
17
+
18
+ export { SETTINGS_MODULE as manifest };
19
+
20
+ export function SettingsPage({ tabs: extraTabs = [], defaultTab = 'design', tokens, style }) {
21
+ const { tokens: currentTokens, updateToken, updateTokens, resetTokens, saveTokens, isDirty } = useTokens();
22
+
23
+ const builtinTabs = [
24
+ { id: 'design', label: '🎨 Design Tokens', content: <DesignTab tokens={currentTokens} onChange={updateToken} onReset={resetTokens} onSave={saveTokens} isDirty={isDirty} /> },
25
+ { id: 'account', label: '👤 Account', content: <PlaceholderTab label="Account settings" /> },
26
+ { id: 'billing', label: '💳 Billing', content: <PlaceholderTab label="Billing settings" /> },
27
+ { id: 'api-keys', label: '🔑 API Keys', content: <PlaceholderTab label="API key management" /> },
28
+ ];
29
+
30
+ const allTabs = [...builtinTabs, ...extraTabs];
31
+ const [active, setActive] = useState(defaultTab);
32
+ const activeContent = allTabs.find(t => t.id === active)?.content;
33
+
34
+ return (
35
+ <div style={{
36
+ padding: 'var(--mn-spacing-xl, 32px)',
37
+ display: 'flex', flexDirection: 'column', gap: 24,
38
+ fontFamily: 'var(--mn-font-family, inherit)',
39
+ ...style,
40
+ }}>
41
+ <div>
42
+ <h1 style={{ margin: 0, fontSize: 'var(--mn-font-size-2xl, 1.5rem)', fontWeight: 700, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>Settings</h1>
43
+ <p style={{ margin: '4px 0 0', fontSize: '0.875rem', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>Configure your workspace and platform appearance</p>
44
+ </div>
45
+
46
+ {/* Tab bar */}
47
+ <div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))', paddingBottom: 0 }}>
48
+ {allTabs.map(tab => (
49
+ <button
50
+ key={tab.id}
51
+ onClick={() => setActive(tab.id)}
52
+ style={{
53
+ padding: '8px 16px',
54
+ background: 'none', border: 'none',
55
+ borderBottom: active === tab.id ? '2px solid var(--mn-color-primary, #3B82F6)' : '2px solid transparent',
56
+ color: active === tab.id ? 'var(--mn-text-primary-dark, #F0F4FF)' : 'var(--mn-text-secondary-dark, #94A3B8)',
57
+ fontSize: '0.875rem', fontWeight: 500,
58
+ cursor: 'pointer',
59
+ marginBottom: -1,
60
+ fontFamily: 'inherit',
61
+ transition: 'color 150ms, border-color 150ms',
62
+ }}
63
+ >
64
+ {tab.label}
65
+ </button>
66
+ ))}
67
+ </div>
68
+
69
+ {/* Active tab content */}
70
+ <div>{activeContent}</div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function DesignTab({ tokens, onChange, onReset, onSave, isDirty }) {
76
+ const [copied, setCopied] = useState('');
77
+
78
+ function copy(text, key) {
79
+ navigator.clipboard?.writeText(text).then(() => {
80
+ setCopied(key);
81
+ setTimeout(() => setCopied(''), 2000);
82
+ });
83
+ }
84
+
85
+ return (
86
+ <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 320px', gap: 24, alignItems: 'start' }}>
87
+ {/* Left — editor */}
88
+ <div>
89
+ {/* Actions */}
90
+ <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
91
+ {isDirty && <ActionBtn onClick={onReset} variant="ghost">Reset</ActionBtn>}
92
+ <ActionBtn onClick={onSave} disabled={!isDirty} variant="primary">{isDirty ? 'Save Changes' : 'Saved'}</ActionBtn>
93
+ <ActionBtn onClick={() => copy(tokensToCSS(tokens), 'css')} variant="outline">{copied === 'css' ? '✓ Copied' : 'Copy CSS'}</ActionBtn>
94
+ <ActionBtn onClick={() => copy(JSON.stringify(tokens, null, 2), 'json')} variant="outline">{copied === 'json' ? '✓ Copied' : 'Copy JSON'}</ActionBtn>
95
+ </div>
96
+
97
+ {/* Token groups */}
98
+ {TOKEN_GROUPS.map(group => (
99
+ <TokenGroupSection key={group.id} group={group} tokens={tokens} onChange={onChange} />
100
+ ))}
101
+ </div>
102
+
103
+ {/* Right — live preview */}
104
+ <div style={{ position: 'sticky', top: 70 }}>
105
+ <TokenPreview tokens={tokens} />
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ function TokenGroupSection({ group, tokens }) {
112
+ const [open, setOpen] = useState(true);
113
+ const { updateToken } = useTokens();
114
+
115
+ return (
116
+ <div style={{ marginBottom: 12, borderRadius: 'var(--mn-radius-lg, 0.75rem)', border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))', overflow: 'hidden' }}>
117
+ <button
118
+ onClick={() => setOpen(o => !o)}
119
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '12px 16px', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--mn-text-primary-dark, #F0F4FF)', fontFamily: 'inherit' }}
120
+ >
121
+ <span style={{ fontWeight: 600, fontSize: '0.8125rem' }}>{group.label}</span>
122
+ <span style={{ fontSize: 10, color: 'var(--mn-text-muted-dark, #64748B)' }}>{open ? '▲' : '▼'}</span>
123
+ </button>
124
+
125
+ {open && group.tokens.map(meta => {
126
+ const value = tokens[meta.key] ?? DEFAULT_TOKENS[meta.key];
127
+ const isDefault = value === DEFAULT_TOKENS[meta.key];
128
+ return (
129
+ <div key={meta.key} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '8px 16px', borderTop: '1px solid var(--mn-border-dark, rgba(255,255,255,0.05))' }}>
130
+ <div>
131
+ <p style={{ margin: 0, fontSize: '0.8125rem', fontWeight: 500, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>{meta.label}</p>
132
+ <code style={{ fontSize: 11, color: 'var(--mn-text-muted-dark, #64748B)' }}>{meta.key}</code>
133
+ </div>
134
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
135
+ {!isDefault && (
136
+ <button onClick={() => updateToken(meta.key, DEFAULT_TOKENS[meta.key])} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--mn-text-muted-dark, #64748B)', fontSize: 14 }}>↺</button>
137
+ )}
138
+ {meta.type === 'color' ? (
139
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
140
+ <input type="color" value={value.startsWith?.('#') ? value : '#000000'} onChange={e => updateToken(meta.key, e.target.value)} style={{ width: 28, height: 28, border: 'none', borderRadius: 4, cursor: 'pointer', padding: 0 }} />
141
+ <input type="text" value={value} onChange={e => updateToken(meta.key, e.target.value)} style={{ width: 88, padding: '3px 8px', borderRadius: 6, border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.1))', backgroundColor: 'rgba(255,255,255,0.05)', color: 'var(--mn-text-primary-dark, #F0F4FF)', fontSize: 12, outline: 'none', fontFamily: 'monospace' }} />
142
+ </div>
143
+ ) : (
144
+ <input type="text" value={value} onChange={e => updateToken(meta.key, e.target.value)} style={{ width: 120, padding: '3px 8px', borderRadius: 6, border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.1))', backgroundColor: 'rgba(255,255,255,0.05)', color: 'var(--mn-text-primary-dark, #F0F4FF)', fontSize: 12, outline: 'none' }} />
145
+ )}
146
+ </div>
147
+ </div>
148
+ );
149
+ })}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ function TokenPreview({ tokens }) {
155
+ return (
156
+ <div style={{ borderRadius: 'var(--mn-radius-lg, 0.75rem)', border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))', overflow: 'hidden' }}>
157
+ <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))' }}>
158
+ <p style={{ margin: 0, fontSize: '0.8125rem', fontWeight: 600, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>Live Preview</p>
159
+ </div>
160
+ <div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
161
+ {/* Color swatches */}
162
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
163
+ {['colorPrimary','colorAccent','colorAccentAlt','colorSuccess','colorWarning','colorDanger'].map(k => (
164
+ <div key={k} title={k} style={{ width: 28, height: 28, borderRadius: 6, backgroundColor: tokens[k] ?? DEFAULT_TOKENS[k] }} />
165
+ ))}
166
+ </div>
167
+ {/* Sample button */}
168
+ <button style={{ padding: '8px 16px', borderRadius: 'var(--mn-radius-md, 0.5rem)', backgroundColor: 'var(--mn-color-primary, #3B82F6)', color: '#fff', border: 'none', fontFamily: 'var(--mn-font-family, inherit)', fontSize: '0.875rem', fontWeight: 500, cursor: 'default' }}>Primary Button</button>
169
+ <button style={{ padding: '8px 16px', borderRadius: 'var(--mn-radius-md, 0.5rem)', backgroundColor: 'transparent', color: 'var(--mn-color-primary, #3B82F6)', border: '1px solid var(--mn-color-primary, #3B82F6)', fontFamily: 'var(--mn-font-family, inherit)', fontSize: '0.875rem', fontWeight: 500, cursor: 'default' }}>Outline Button</button>
170
+ {/* Sample card */}
171
+ <div style={{ padding: 12, borderRadius: 'var(--mn-radius-lg, 0.75rem)', backgroundColor: 'var(--mn-color-card-dark, #0B0F23)', border: '1px solid var(--mn-border-dark, rgba(255,255,255,0.07))' }}>
172
+ <p style={{ margin: 0, fontSize: '0.875rem', fontWeight: 600, color: 'var(--mn-text-primary-dark, #F0F4FF)' }}>Card surface</p>
173
+ <p style={{ margin: '4px 0 0', fontSize: '0.75rem', color: 'var(--mn-text-secondary-dark, #94A3B8)' }}>Secondary text color</p>
174
+ </div>
175
+ {/* Radius preview */}
176
+ <div style={{ display: 'flex', gap: 8 }}>
177
+ {['radiusSm','radiusMd','radiusLg','radiusXl'].map(k => (
178
+ <div key={k} style={{ width: 32, height: 32, backgroundColor: 'var(--mn-color-accent, #8B5CF6)', borderRadius: tokens[k] ?? DEFAULT_TOKENS[k] }} />
179
+ ))}
180
+ </div>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function ActionBtn({ children, onClick, variant = 'primary', disabled = false }) {
187
+ const styles = {
188
+ primary: { backgroundColor: 'var(--mn-color-primary, #3B82F6)', color: '#fff', border: 'none' },
189
+ ghost: { backgroundColor: 'rgba(255,255,255,0.06)', color: 'var(--mn-text-secondary-dark, #94A3B8)', border: 'none' },
190
+ outline: { backgroundColor: 'transparent', color: 'var(--mn-text-secondary-dark, #94A3B8)', border: '1px solid var(--mn-border-md-dark, rgba(255,255,255,0.13))' },
191
+ };
192
+ return (
193
+ <button onClick={onClick} disabled={disabled} style={{ ...styles[variant], padding: '6px 14px', borderRadius: 'var(--mn-radius-md, 0.5rem)', fontSize: '0.8125rem', fontWeight: 500, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1, fontFamily: 'inherit' }}>
194
+ {children}
195
+ </button>
196
+ );
197
+ }
198
+
199
+ function PlaceholderTab({ label }) {
200
+ return (
201
+ <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--mn-text-muted-dark, #64748B)', fontSize: '0.875rem' }}>
202
+ {label} — configure with your own content
203
+ </div>
204
+ );
205
+ }