@micropage-sh/cli 2.0.0 → 2.0.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micropage-sh/cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.7",
4
4
  "description": "CLI for micropage.sh - create, sync, and publish microsites",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const {
11
11
  getValidAccessToken,
12
12
  getMaxDeployEventIdForBuild,
13
13
  streamDeployEventsUntilDone,
14
+ uploadAssetsWithToken,
14
15
  } = require('../supabase');
15
16
  const { getProjectConfig, setProjectConfig } = require('../auth');
16
17
  const { readPageFilesFromDir } = require('../parser');
@@ -139,6 +140,23 @@ async function push(options = {}) {
139
140
  process.exit(1);
140
141
  }
141
142
 
143
+ // Upload new files in assets/ to project Files before parsing,
144
+ // so `img: <- name` lookups can resolve against just-uploaded files.
145
+ try {
146
+ const accessToken = await getValidAccessToken();
147
+ const uploaded = await uploadAssetsWithToken(
148
+ accessToken,
149
+ config.projectId,
150
+ cwd,
151
+ (filename) => console.log(`Uploaded asset: ${filename}`),
152
+ );
153
+ if (uploaded > 0) console.log(`Uploaded ${uploaded} asset(s).`);
154
+ } catch (err) {
155
+ handleAuthError(err);
156
+ console.error('Asset upload failed:', err.message);
157
+ process.exit(1);
158
+ }
159
+
142
160
  // Parse content via parser API
143
161
  console.log('Parsing content…');
144
162
  let jsonContent;
@@ -3,13 +3,7 @@
3
3
  const { execSync } = require('child_process');
4
4
  const { db, handleAuthError } = require('../supabase');
5
5
  const { getProjectConfig } = require('../auth');
6
- const { APP_URL } = require('../config');
7
-
8
- function projectUrl(domain) {
9
- if (!domain) return null;
10
- if (domain.startsWith('http')) return domain;
11
- return `https://${domain}.pages.dev`;
12
- }
6
+ const { APP_URL, projectUrl } = require('../config');
13
7
 
14
8
  function copyToClipboard(text) {
15
9
  const cmds = [
@@ -41,7 +35,7 @@ async function getProject(cwd) {
41
35
  try {
42
36
  const project = await db
43
37
  .from('projects')
44
- .select('id,name,domain')
38
+ .select('id,name,domain,custom_domain')
45
39
  .eq('id', config.projectId)
46
40
  .single();
47
41
  return project || config;
@@ -56,7 +50,7 @@ async function getProject(cwd) {
56
50
  async function preview() {
57
51
  const cwd = process.cwd();
58
52
  const project = await getProject(cwd);
59
- const url = projectUrl(project.domain);
53
+ const url = projectUrl(project.domain, project.custom_domain);
60
54
 
61
55
  if (!url) {
62
56
  console.error('No domain set for this project. Publish first.');
@@ -76,7 +70,7 @@ async function preview() {
76
70
  async function copyLink() {
77
71
  const cwd = process.cwd();
78
72
  const project = await getProject(cwd);
79
- const url = projectUrl(project.domain);
73
+ const url = projectUrl(project.domain, project.custom_domain);
80
74
 
81
75
  if (!url) {
82
76
  console.error('No domain set for this project. Publish first.');
@@ -2,9 +2,10 @@
2
2
 
3
3
  const http = require('http');
4
4
  const { randomBytes } = require('crypto');
5
- const { setSession, getSession } = require('../auth');
5
+ const { setSession, getSession, clearSession } = require('../auth');
6
6
  const { getUserInfo, getValidAccessToken } = require('../supabase');
7
7
  const { APP_URL, SUPABASE_URL, SUPABASE_ANON_KEY } = require('../config');
8
+ const { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken } = require('../plan');
8
9
 
9
10
  // Find an available ephemeral port by briefly binding to port 0.
10
11
  function getFreePort() {
@@ -164,11 +165,24 @@ async function run(options = {}) {
164
165
  const existing = getSession();
165
166
  if (existing) {
166
167
  try {
167
- await getValidAccessToken();
168
- const email = existing.user?.email || 'unknown';
169
- console.log(`Already logged in as ${email}.`);
170
- console.log('Use --force to re-authenticate.');
171
- return;
168
+ const accessToken = await getValidAccessToken();
169
+ const planTier = await getPlanTierWithToken(existing.user?.id, accessToken);
170
+ if (!isPaidTier(planTier)) {
171
+ clearSession();
172
+ console.error(UPGRADE_MESSAGE);
173
+ process.exit(1);
174
+ }
175
+ const displayName = existing.user?.email
176
+ || existing.user?.user_metadata?.full_name
177
+ || existing.user?.user_metadata?.user_name
178
+ || existing.user?.user_metadata?.name
179
+ || existing.user?.id;
180
+ if (displayName) {
181
+ console.log(`Already logged in as ${displayName}.`);
182
+ console.log('Use --force to re-authenticate.');
183
+ process.exit(0);
184
+ }
185
+ // No identifiable display name on the cached user — fall through to a fresh login.
172
186
  } catch {
173
187
  // Token expired or refresh failed — fall through to re-login
174
188
  }
@@ -240,10 +254,27 @@ async function run(options = {}) {
240
254
  server.close();
241
255
 
242
256
  const user = await getUserInfo(access_token);
257
+ if (!user?.id) {
258
+ console.error('Login failed: could not load user profile.');
259
+ process.exit(1);
260
+ }
261
+
262
+ const planTier = await getPlanTierWithToken(user.id, access_token);
263
+ if (!isPaidTier(planTier)) {
264
+ // Free-tier (or unconfirmed) users get no session at all.
265
+ console.error(UPGRADE_MESSAGE);
266
+ process.exit(1);
267
+ }
268
+
243
269
  setSession({ access_token, refresh_token, user });
244
270
 
245
- const displayName = user?.email || user?.user_metadata?.user_name || 'unknown';
271
+ const displayName = user.email
272
+ || user.user_metadata?.full_name
273
+ || user.user_metadata?.user_name
274
+ || user.user_metadata?.name
275
+ || user.id;
246
276
  console.log(`Logged in as ${displayName}`);
277
+ process.exit(0);
247
278
  } catch (err) {
248
279
  clearTimeout(timeout);
249
280
  server.close();
@@ -9,8 +9,16 @@ function run() {
9
9
  return;
10
10
  }
11
11
  clearSession();
12
- const email = session.user?.email || 'unknown';
13
- console.log(`Logged out (was: ${email}).`);
12
+ const displayName = session.user?.email
13
+ || session.user?.user_metadata?.full_name
14
+ || session.user?.user_metadata?.user_name
15
+ || session.user?.user_metadata?.name
16
+ || session.user?.id;
17
+ if (displayName) {
18
+ console.log(`Logged out (was: ${displayName}).`);
19
+ } else {
20
+ console.log('Logged out.');
21
+ }
14
22
  }
15
23
 
16
24
  module.exports = { run };
@@ -23,11 +23,9 @@ const { syncAssets } = require('./files');
23
23
  // Helpers
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
- function projectUrl(host) {
27
- if (!host) return '-';
28
- if (host.startsWith('http://') || host.startsWith('https://')) return host;
29
- return `https://${host}`;
30
- }
26
+ const { projectUrl: buildProjectUrl } = require('../config');
27
+ const projectUrl = (slug, customDomain) =>
28
+ buildProjectUrl(slug, customDomain) || '-';
31
29
 
32
30
  function copyIfExists(src, dest) {
33
31
  try {
@@ -49,26 +47,69 @@ function copyExamplesAndAssetsIntoProject(dir) {
49
47
  fs.mkdirSync(examplesDest, { recursive: true });
50
48
  }
51
49
 
52
- // Copy example .page files
50
+ // Collect full-page templates (exclude components-* demo files) for the starter pick.
51
+ // All examples are still copied into examples/ for the user to browse.
52
+ const allExampleFiles = [];
53
+ const starterCandidates = [];
53
54
  try {
54
55
  const files = fs.readdirSync(examplesSrc);
55
56
  for (const file of files) {
56
57
  if (!file.endsWith('.page')) continue;
57
- const src = path.join(examplesSrc, file);
58
- const dest = path.join(examplesDest, file);
59
- copyIfExists(src, dest);
58
+ allExampleFiles.push(file);
59
+ if (!file.startsWith('components-')) {
60
+ starterCandidates.push(file);
61
+ }
60
62
  }
61
63
  } catch {
62
64
  // ignore if templates/examples missing in this build
63
65
  }
64
66
 
65
- // Default logo / favicon in assets/ (uploaded on push as logo.svg / favicon.svg; examples use `logo: <- logo.svg`)
67
+ for (const file of allExampleFiles) {
68
+ const src = path.join(examplesSrc, file);
69
+ const dest = path.join(examplesDest, file);
70
+ copyIfExists(src, dest);
71
+ }
72
+
73
+ // Write a randomized starter landing.page at the project root.
74
+ if (starterCandidates.length > 0) {
75
+ const pick = starterCandidates[Math.floor(Math.random() * starterCandidates.length)];
76
+ copyIfExists(path.join(examplesSrc, pick), path.join(dir, 'landing.page'));
77
+ }
78
+
79
+ // Logo / favicon in assets/ — pick a random variant pair from logos/ and favicons/.
80
+ // Falls back to the root logo.svg / favicon.svg if the variant directories are absent or empty.
66
81
  const destAssets = path.join(dir, 'assets');
67
82
  if (!fs.existsSync(destAssets)) {
68
83
  fs.mkdirSync(destAssets, { recursive: true });
69
84
  }
70
- copyIfExists(path.join(assetsSrc, 'logo.svg'), path.join(destAssets, 'logo.svg'));
71
- copyIfExists(path.join(assetsSrc, 'favicon.svg'), path.join(destAssets, 'favicon.svg'));
85
+
86
+ const logosDir = path.join(assetsSrc, 'logos');
87
+ const faviconsDir = path.join(assetsSrc, 'favicons');
88
+ let usedVariant = false;
89
+
90
+ try {
91
+ if (fs.existsSync(logosDir)) {
92
+ const logoFiles = fs.readdirSync(logosDir).filter((f) => /^logo-[^.]+\.svg$/.test(f));
93
+ if (logoFiles.length > 0) {
94
+ const chosen = logoFiles[Math.floor(Math.random() * logoFiles.length)];
95
+ // logo-x.svg -> favicon-x.svg
96
+ const variantId = chosen.replace(/^logo-/, '').replace(/\.svg$/, '');
97
+ const matchingFavicon = path.join(faviconsDir, `favicon-${variantId}.svg`);
98
+ if (fs.existsSync(matchingFavicon)) {
99
+ copyIfExists(path.join(logosDir, chosen), path.join(destAssets, 'logo.svg'));
100
+ copyIfExists(matchingFavicon, path.join(destAssets, 'favicon.svg'));
101
+ usedVariant = true;
102
+ }
103
+ }
104
+ }
105
+ } catch {
106
+ // fall through to default below
107
+ }
108
+
109
+ if (!usedVariant) {
110
+ copyIfExists(path.join(assetsSrc, 'logo.svg'), path.join(destAssets, 'logo.svg'));
111
+ copyIfExists(path.join(assetsSrc, 'favicon.svg'), path.join(destAssets, 'favicon.svg'));
112
+ }
72
113
  }
73
114
 
74
115
  async function getLatestBuild(projectId) {
@@ -145,7 +186,7 @@ async function list(options = {}) {
145
186
 
146
187
  const rows = projects.map((p) => {
147
188
  const build = latestBuild[p.id];
148
- const url = projectUrl(p.custom_domain || p.domain);
189
+ const url = projectUrl(p.domain, p.custom_domain);
149
190
  return [
150
191
  p.uuid || '-',
151
192
  p.name || '-',
@@ -195,7 +236,7 @@ async function show(uuidOrDomain, options = {}) {
195
236
  return;
196
237
  }
197
238
 
198
- const url = projectUrl(project.custom_domain || project.domain);
239
+ const url = projectUrl(project.domain, project.custom_domain);
199
240
 
200
241
  console.log('UUID: ', project.uuid || '-');
201
242
  console.log('Name: ', project.name || '-');
@@ -261,9 +302,9 @@ async function create(name, options = {}) {
261
302
 
262
303
  console.log('Created project:', project.name || name);
263
304
  if (project.uuid) console.log('UUID:', project.uuid);
264
- console.log('URL:', projectUrl(project.domain));
305
+ console.log('URL:', projectUrl(project.domain, project.custom_domain));
265
306
  console.log('Folder:', dir);
266
- console.log(`\nNext: cd ${name} && touch landing.page && micropage push`);
307
+ console.log(`\nNext: cd ${name} && micropage push`);
267
308
  }
268
309
 
269
310
  // ---------------------------------------------------------------------------
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { getSession } = require('../auth');
3
+ const { getSession, clearSession } = require('../auth');
4
4
  const { db, getUserInfo, getValidAccessToken, handleAuthError } = require('../supabase');
5
+ const { UPGRADE_MESSAGE, isPaidTier } = require('../plan');
5
6
 
6
7
  async function run() {
7
8
  const session = getSession();
@@ -25,35 +26,41 @@ async function run() {
25
26
  process.exit(1);
26
27
  }
27
28
 
28
- console.log('User ID: ', user.id);
29
- console.log('Email: ', user.email || '-');
30
- const name =
31
- user.user_metadata?.full_name ||
32
- user.user_metadata?.user_name ||
33
- user.user_metadata?.name ||
34
- '-';
35
- console.log('Name: ', name);
36
- if (user.app_metadata?.provider) {
37
- console.log('Auth via: ', user.app_metadata.provider);
38
- }
39
-
40
- // Billing / subscription overview (best-effort; do not fail whoami if this breaks).
29
+ // Gate access on plan first — free-tier accounts get no session and no detail dump.
41
30
  let planTier = 'free';
42
- let subscription = null;
43
31
  try {
44
32
  const customer = await db
45
33
  .from('customers')
46
34
  .select('plan_tier')
47
35
  .eq('user_id', user.id)
48
36
  .single();
49
- if (customer?.plan_tier) {
37
+ if (customer?.plan_tier === 'pro' || customer?.plan_tier === 'pro_plus') {
50
38
  planTier = customer.plan_tier;
51
39
  }
52
40
  } catch (err) {
53
41
  handleAuthError(err);
54
- console.error('Warning: failed to load plan info:', err.message);
42
+ // If we cannot determine plan, default to free → deny CLI access below.
55
43
  }
56
44
 
45
+ if (!isPaidTier(planTier)) {
46
+ clearSession();
47
+ console.error(UPGRADE_MESSAGE);
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log('User ID: ', user.id);
52
+ console.log('Email: ', user.email || '-');
53
+ const name =
54
+ user.user_metadata?.full_name ||
55
+ user.user_metadata?.user_name ||
56
+ user.user_metadata?.name ||
57
+ '-';
58
+ console.log('Name: ', name);
59
+ if (user.app_metadata?.provider) {
60
+ console.log('Auth via: ', user.app_metadata.provider);
61
+ }
62
+
63
+ let subscription = null;
57
64
  try {
58
65
  const subs = await db
59
66
  .from('subscriptions')
package/src/config.js CHANGED
@@ -19,4 +19,30 @@ const BUILD_COMPILER_URL =
19
19
  process.env.MICROPAGE_PARSER_URL ||
20
20
  'https://build-compiler.micropage.sh';
21
21
 
22
- module.exports = { SUPABASE_URL, SUPABASE_ANON_KEY, APP_URL, BUILD_COMPILER_URL };
22
+ const BASE_DOMAIN = process.env.MICROPAGE_BASE_DOMAIN || 'micropage.sh';
23
+
24
+ // Live URL for a project. Prefers the customer's custom domain (a full FQDN
25
+ // like `www.example.com`) when set; falls back to `https://<slug>.<BASE_DOMAIN>`.
26
+ // Returns null when there's no usable identifier.
27
+ function projectUrl(slug, customDomain) {
28
+ const cd = customDomain && String(customDomain).trim();
29
+ if (cd) {
30
+ return cd.startsWith('http://') || cd.startsWith('https://')
31
+ ? cd
32
+ : `https://${cd}`;
33
+ }
34
+ if (!slug) return null;
35
+ const s = String(slug).trim();
36
+ if (!s) return null;
37
+ if (s.startsWith('http://') || s.startsWith('https://')) return s;
38
+ return `https://${s}.${BASE_DOMAIN}`;
39
+ }
40
+
41
+ module.exports = {
42
+ SUPABASE_URL,
43
+ SUPABASE_ANON_KEY,
44
+ APP_URL,
45
+ BUILD_COMPILER_URL,
46
+ BASE_DOMAIN,
47
+ projectUrl,
48
+ };
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const { Command } = require('commander');
5
+ const { version } = require('../package.json');
5
6
 
6
7
  const login = require('./commands/login');
7
8
  const logout = require('./commands/logout');
@@ -18,7 +19,7 @@ const program = new Command();
18
19
  program
19
20
  .name('micropage')
20
21
  .description('CLI for micropage.sh – create, sync, and publish microsites')
21
- .version('2.0.0');
22
+ .version(version);
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Authentication
package/src/parser.js CHANGED
@@ -48,18 +48,15 @@ function writePageFile(dir, content) {
48
48
  return dest;
49
49
  }
50
50
 
51
- const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg'];
52
-
53
51
  function listAssetsFromDir(dir) {
54
52
  const assetsDir = path.join(dir, 'assets');
55
53
  if (!fs.existsSync(assetsDir)) return [];
56
54
  const entries = fs.readdirSync(assetsDir, { withFileTypes: true });
57
55
  const files = [];
58
56
  for (const e of entries) {
59
- if (e.isFile()) {
60
- const ext = path.extname(e.name).toLowerCase();
61
- if (ASSET_EXTENSIONS.includes(ext)) files.push(e.name);
62
- }
57
+ if (!e.isFile()) continue;
58
+ if (e.name.startsWith('.')) continue;
59
+ files.push(e.name);
63
60
  }
64
61
  return files.sort();
65
62
  }
@@ -68,5 +65,4 @@ module.exports = {
68
65
  readPageFilesFromDir,
69
66
  writePageFile,
70
67
  listAssetsFromDir,
71
- ASSET_EXTENSIONS,
72
68
  };
package/src/plan.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
4
+
5
+ const UPGRADE_MESSAGE = [
6
+ 'The micropage CLI is available on paid plans only.',
7
+ 'Your account is currently on the free plan.',
8
+ '',
9
+ 'Upgrade at: https://micropage.sh/pricing',
10
+ ].join('\n');
11
+
12
+ function isPaidTier(tier) {
13
+ return tier === 'pro' || tier === 'pro_plus';
14
+ }
15
+
16
+ // Look up plan_tier using a bearer token directly so this can be called inside
17
+ // `login` before a session is persisted. Defaults to 'free' on any failure —
18
+ // CLI access is denied when we cannot confirm a paid tier.
19
+ async function getPlanTierWithToken(userId, accessToken) {
20
+ try {
21
+ const url = `${SUPABASE_URL}/rest/v1/customers?select=plan_tier&user_id=eq.${encodeURIComponent(userId)}&limit=1`;
22
+ const res = await fetch(url, {
23
+ headers: {
24
+ apikey: SUPABASE_ANON_KEY,
25
+ Authorization: `Bearer ${accessToken}`,
26
+ },
27
+ });
28
+ if (!res.ok) return 'free';
29
+ const data = await res.json();
30
+ const row = Array.isArray(data) ? data[0] : null;
31
+ if (row?.plan_tier === 'pro' || row?.plan_tier === 'pro_plus') {
32
+ return row.plan_tier;
33
+ }
34
+ return 'free';
35
+ } catch {
36
+ return 'free';
37
+ }
38
+ }
39
+
40
+ module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken };
package/src/supabase.js CHANGED
@@ -257,13 +257,23 @@ const db = {
257
257
  // ---------------------------------------------------------------------------
258
258
 
259
259
  async function getUserInfo(accessToken) {
260
- const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
261
- headers: {
262
- 'apikey': SUPABASE_ANON_KEY,
263
- 'Authorization': `Bearer ${accessToken}`,
264
- },
265
- });
266
- if (!res.ok) return null;
260
+ let res;
261
+ try {
262
+ res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
263
+ headers: {
264
+ 'apikey': SUPABASE_ANON_KEY,
265
+ 'Authorization': `Bearer ${accessToken}`,
266
+ },
267
+ });
268
+ } catch (err) {
269
+ console.error(`[micropage] getUserInfo network error: ${err.message}`);
270
+ return null;
271
+ }
272
+ if (!res.ok) {
273
+ const body = await res.text().catch(() => '');
274
+ console.error(`[micropage] getUserInfo failed (HTTP ${res.status} from ${SUPABASE_URL}/auth/v1/user): ${body}`);
275
+ return null;
276
+ }
267
277
  return res.json();
268
278
  }
269
279
 
@@ -634,4 +644,5 @@ module.exports = {
634
644
  exchangeDeployTokenForAccessToken,
635
645
  pushWithToken,
636
646
  invokePublishBuild,
647
+ uploadAssetsWithToken,
637
648
  };
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
2
+ <title>Micropage favicon — variant A</title>
3
+ <desc>Minimal stacked bar favicon for starter projects</desc>
4
+ <rect x="4" y="7" width="24" height="4" rx="2" fill="#4f46e5"/>
5
+ <rect x="6" y="14" width="20" height="4" rx="2" fill="#0ea5e9"/>
6
+ <rect x="8" y="21" width="16" height="4" rx="2" fill="#22c55e"/>
7
+ </svg>
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
2
+ <title>Micropage favicon — variant B</title>
3
+ <desc>Minimal sunrise arc favicon</desc>
4
+ <defs>
5
+ <linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
6
+ <stop offset="0%" stop-color="#dc2626"/>
7
+ <stop offset="100%" stop-color="#f59e0b"/>
8
+ </linearGradient>
9
+ </defs>
10
+ <path d="M 4 22 A 12 12 0 0 1 28 22" stroke="url(#g)" stroke-width="5" fill="none" stroke-linecap="round"/>
11
+ <circle cx="16" cy="22" r="3" fill="#dc2626"/>
12
+ </svg>
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
2
+ <title>Micropage favicon — variant C</title>
3
+ <desc>Minimal concentric circles favicon in forest green</desc>
4
+ <circle cx="16" cy="14" r="10" fill="#166534"/>
5
+ <circle cx="16" cy="14" r="5" fill="#14532d"/>
6
+ <circle cx="16" cy="14" r="2" fill="#bbf7d0"/>
7
+ <rect x="14" y="24" width="4" height="6" rx="2" fill="#713f12"/>
8
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
2
+ <title>Micropage favicon — variant D</title>
3
+ <desc>Minimal diamond favicon in charcoal with magenta center</desc>
4
+ <polygon points="16,4 28,16 16,28 4,16" fill="#1c1c1c"/>
5
+ <polygon points="16,11 21,16 16,21 11,16" fill="#e91e8c"/>
6
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
2
+ <title>Micropage starter logo — variant A</title>
3
+ <desc>Abstract stacked rectangles suggesting pages</desc>
4
+ <defs>
5
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
6
+ <stop offset="0%" stop-color="#4f46e5"/>
7
+ <stop offset="100%" stop-color="#06b6d4"/>
8
+ </linearGradient>
9
+ </defs>
10
+ <rect x="10" y="14" width="44" height="12" rx="3" fill="url(#g)"/>
11
+ <rect x="14" y="28" width="36" height="10" rx="3" fill="#111827" opacity="0.9"/>
12
+ <rect x="18" y="40" width="28" height="8" rx="3" fill="#111827" opacity="0.7"/>
13
+ </svg>
@@ -0,0 +1,16 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
2
+ <title>Micropage starter logo — variant B</title>
3
+ <desc>Abstract sunrise arc suggesting warmth and momentum</desc>
4
+ <defs>
5
+ <linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
6
+ <stop offset="0%" stop-color="#dc2626"/>
7
+ <stop offset="50%" stop-color="#ea580c"/>
8
+ <stop offset="100%" stop-color="#f59e0b"/>
9
+ </linearGradient>
10
+ </defs>
11
+ <path d="M 8 44 A 24 24 0 0 1 56 44" stroke="url(#g)" stroke-width="8" fill="none" stroke-linecap="round"/>
12
+ <circle cx="32" cy="44" r="5" fill="#dc2626"/>
13
+ <line x1="32" y1="14" x2="32" y2="22" stroke="#f59e0b" stroke-width="4" stroke-linecap="round"/>
14
+ <line x1="14" y1="26" x2="20" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
15
+ <line x1="50" y1="26" x2="44" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
16
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
2
+ <title>Micropage starter logo — variant C</title>
3
+ <desc>Abstract leaf and root mark suggesting growth and earth</desc>
4
+ <circle cx="32" cy="20" r="14" fill="#166534"/>
5
+ <circle cx="32" cy="20" r="8" fill="#14532d"/>
6
+ <circle cx="32" cy="20" r="4" fill="#bbf7d0"/>
7
+ <rect x="30" y="34" width="4" height="14" rx="2" fill="#713f12"/>
8
+ <path d="M 20 48 Q 32 38 44 48" stroke="#713f12" stroke-width="3" fill="none" stroke-linecap="round"/>
9
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
2
+ <title>Micropage starter logo — variant D</title>
3
+ <desc>Bold geometric diamond mark in charcoal with magenta accent</desc>
4
+ <polygon points="32,8 52,32 32,56 12,32" fill="#1c1c1c"/>
5
+ <polygon points="32,18 44,32 32,46 20,32" fill="#2e2e2e"/>
6
+ <polygon points="32,26 38,32 32,38 26,32" fill="#e91e8c"/>
7
+ </svg>