@keenmate/pure-admin-core 2.3.5 → 2.4.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.
package/README.md CHANGED
@@ -2,17 +2,16 @@
2
2
 
3
3
  Lightweight, data-focused CSS/SCSS admin framework with Corporate theme as default.
4
4
 
5
+ ## What's New in 2.3.6
6
+
7
+ - **Responsive font sizing** — `pa-font-responsive` class on `<html>` for automatic mobile scaling (10px desktop, 12px mobile). Granular `pa-font-base-*` / `pa-font-mobile-*` classes for full control. No JS, no FOUC.
8
+ - **Getting Started page** — New demo page covering installation, theme management via CLI, responsive sizing, RTL, and BEM reference
9
+
5
10
  ## What's New in 2.3.5
6
11
 
7
12
  - **Navbar alignment fix** — `__end` section pushed to right edge via `margin-inline-start: auto`, works without `__center` spacer
8
13
  - **Scroll-lock fix** — Panel/modal open no longer hides scrollbar (`overflow-y: scroll` instead of `hidden`)
9
14
 
10
- ## What's New in 2.3.4
11
-
12
- - **Command palette home screen & hotkeys** — Opens with categorized commands and search contexts. Alt+D/A/G/T hotkeys jump directly into commands. Global search finds commands alongside data. Form codes for quick `/go` navigation.
13
- - **Dropdown z-index fix** — Split button menus no longer go under sidebar/header
14
- - **Removed hover lift** — `translateY(-1px)` removed from buttons and stat cards for consistency
15
-
16
15
  ## Installation
17
16
 
18
17
  ```bash
@@ -35,16 +34,19 @@ import '@keenmate/pure-admin-core/dist/css/main.css';
35
34
 
36
35
  ### Using a Theme
37
36
 
38
- Themes are separate packages. Install and import:
37
+ Download themes via the CLI and link the CSS:
39
38
 
40
39
  ```bash
41
- npm install @keenmate/pure-admin-theme-audi
40
+ npm install -g @keenmate/pureadmin
41
+ pureadmin themes audi
42
42
  ```
43
43
 
44
44
  ```html
45
- <link rel="stylesheet" href="node_modules/@keenmate/pure-admin-theme-audi/dist/audi.css">
45
+ <link rel="stylesheet" href="static/themes/audi/audi.css">
46
46
  ```
47
47
 
48
+ Browse all themes at [pureadmin.io](https://pureadmin.io). Keep them updated with `pureadmin update`.
49
+
48
50
  ### SCSS Customization
49
51
 
50
52
  ```scss
@@ -107,40 +109,29 @@ Themes are maintained in the separate [`pure-admin-themes`](https://github.com/K
107
109
 
108
110
  ### Theme Setup
109
111
 
110
- Themes are configured via `themes.json` (base config) and `.themes.json` (local overrides, gitignored). Each entry is a theme name mapped to an options object:
111
-
112
- ```json
113
- {
114
- "themes": {
115
- "audi": {},
116
- "corporate": { "path": "../pure-admin-themes/corporate" },
117
- "custom": { "url": "https://my-server.com/themes/custom.zip" }
118
- }
119
- }
120
- ```
121
-
122
- - **`{}`** — downloaded from pureadmin.io bundle
123
- - **`{ "path": "..." }`** — use a local directory (must contain `dist/{name}.css`)
124
- - **`{ "url": "..." }`** — downloaded from a custom URL
125
-
126
- Then run:
112
+ Themes are managed via the [`pureadmin` CLI](https://www.npmjs.com/package/@keenmate/pureadmin):
127
113
 
128
114
  ```bash
129
- npx download-themes
115
+ npm install -D @keenmate/pureadmin
116
+
117
+ npx pureadmin themes list # browse all themes available on pureadmin.io
118
+ npx pureadmin themes add audi corporate # download + register themes in this project
119
+ npx pureadmin themes update # re-download only themes whose content changed
120
+ npx pureadmin themes list --local # show themes configured in this project
130
121
  ```
131
122
 
132
- Or add to your `package.json` scripts:
123
+ The CLI extracts theme ZIPs into `static/themes/{name}/` (configurable via `--dir`) and tracks each one in `pureadmin.json` with version + content hash for change detection. See `npx pureadmin help themes` for the full subcommand reference.
124
+
125
+ For local development against a sibling theme repo, you can hand-edit `.pureadmin.json` (gitignored) and point a slug at a filesystem path:
133
126
 
134
127
  ```json
135
128
  {
136
- "scripts": {
137
- "download-themes": "download-themes"
129
+ "themes": {
130
+ "corporate": "../pure-admin-themes/corporate"
138
131
  }
139
132
  }
140
133
  ```
141
134
 
142
- This fetches all remote themes into `./themes/{name}/` and leaves local paths unchanged. Config files are never modified.
143
-
144
135
  ### pureadmin.io Theme API
145
136
 
146
137
  - `GET /api/theme/{name}` — download a specific theme (e.g. `/api/theme/audi`)
package/dist/css/main.css CHANGED
@@ -6414,28 +6414,28 @@ a.pa-card p {
6414
6414
  white-space: nowrap;
6415
6415
  }
6416
6416
  .pa-stat--square.pa-stat--primary {
6417
- background-color: #007bff;
6418
- color: #ffffff;
6417
+ background-color: var(--pa-accent);
6418
+ color: var(--pa-btn-primary-text);
6419
6419
  }
6420
6420
  .pa-stat--square.pa-stat--success {
6421
- background-color: #28a745;
6422
- color: #ffffff;
6421
+ background-color: var(--pa-success-bg);
6422
+ color: var(--pa-btn-success-text);
6423
6423
  }
6424
6424
  .pa-stat--square.pa-stat--info {
6425
- background-color: #17a2b8;
6426
- color: #ffffff;
6425
+ background-color: var(--pa-info-bg);
6426
+ color: var(--pa-btn-info-text);
6427
6427
  }
6428
6428
  .pa-stat--square.pa-stat--warning {
6429
- background-color: #ffc107;
6430
- color: #212529;
6429
+ background-color: var(--pa-warning-bg);
6430
+ color: var(--pa-btn-warning-text);
6431
6431
  }
6432
6432
  .pa-stat--square.pa-stat--danger {
6433
- background-color: #dc3545;
6434
- color: #ffffff;
6433
+ background-color: var(--pa-danger-bg);
6434
+ color: var(--pa-btn-danger-text);
6435
6435
  }
6436
6436
  .pa-stat--square.pa-stat--secondary {
6437
6437
  background-color: var(--pa-text-color-2);
6438
- color: #ffffff;
6438
+ color: var(--pa-btn-primary-text);
6439
6439
  }
6440
6440
 
6441
6441
  .pa-kpi-grid {
@@ -12325,7 +12325,7 @@ code {
12325
12325
  margin: 0 0 0.4rem 0;
12326
12326
  font-size: 1.8rem;
12327
12327
  font-weight: 600;
12328
- color: var(--pa-text-color-1);
12328
+ color: var(--pa-header-profile-name-color);
12329
12329
  overflow: hidden;
12330
12330
  text-overflow: ellipsis;
12331
12331
  white-space: nowrap;
@@ -12333,7 +12333,8 @@ code {
12333
12333
  .pa-profile-panel__email {
12334
12334
  margin: 0 0 0.8rem 0;
12335
12335
  font-size: 1.4rem;
12336
- color: var(--pa-text-color-2);
12336
+ color: var(--pa-header-profile-name-color);
12337
+ opacity: 0.75;
12337
12338
  overflow: hidden;
12338
12339
  text-overflow: ellipsis;
12339
12340
  white-space: nowrap;
@@ -12342,7 +12343,8 @@ code {
12342
12343
  display: inline-block;
12343
12344
  padding: 0.8rem 1.2rem;
12344
12345
  background-color: var(--pa-accent-light);
12345
- color: var(--pa-accent);
12346
+ background-color: color-mix(in srgb, var(--pa-header-profile-name-color) 15%, transparent);
12347
+ color: var(--pa-header-profile-name-color);
12346
12348
  font-size: 1.2rem;
12347
12349
  font-weight: 500;
12348
12350
  border-radius: var(--pa-border-radius);
@@ -12442,15 +12444,16 @@ code {
12442
12444
  margin-bottom: 0;
12443
12445
  }
12444
12446
  .pa-profile-panel__tabs .pa-tabs__item {
12445
- color: var(--pa-header-text-secondary);
12447
+ color: var(--pa-header-profile-name-color);
12448
+ opacity: 0.6;
12446
12449
  border-bottom-color: transparent;
12447
12450
  }
12448
12451
  .pa-profile-panel__tabs .pa-tabs__item:hover {
12449
- color: var(--pa-header-text);
12450
- background-color: var(--pa-accent-light);
12452
+ opacity: 0.85;
12453
+ background-color: color-mix(in srgb, var(--pa-header-profile-name-color) 10%, transparent);
12451
12454
  }
12452
12455
  .pa-profile-panel__tabs .pa-tabs__item--active {
12453
- color: var(--pa-header-text);
12456
+ opacity: 1;
12454
12457
  border-bottom-color: var(--pa-accent);
12455
12458
  }
12456
12459
  .pa-profile-panel__tabs--icon-only .pa-profile-panel__tab-text {
@@ -16669,6 +16672,39 @@ html.font-size-xlarge {
16669
16672
  font-size: 12px;
16670
16673
  }
16671
16674
 
16675
+ html.pa-font-base-9 {
16676
+ font-size: 9px;
16677
+ }
16678
+
16679
+ html.pa-font-base-10 {
16680
+ font-size: 10px;
16681
+ }
16682
+
16683
+ html.pa-font-base-11 {
16684
+ font-size: 11px;
16685
+ }
16686
+
16687
+ html.pa-font-base-12 {
16688
+ font-size: 12px;
16689
+ }
16690
+
16691
+ @media (max-width: 768px) {
16692
+ html.pa-font-mobile-9 {
16693
+ font-size: 9px;
16694
+ }
16695
+ html.pa-font-mobile-10 {
16696
+ font-size: 10px;
16697
+ }
16698
+ html.pa-font-mobile-11 {
16699
+ font-size: 11px;
16700
+ }
16701
+ html.pa-font-mobile-12 {
16702
+ font-size: 12px;
16703
+ }
16704
+ html.pa-font-responsive {
16705
+ font-size: 12px;
16706
+ }
16707
+ }
16672
16708
  .font-family-serif {
16673
16709
  font-family: Georgia, "Times New Roman", Times, serif;
16674
16710
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/pure-admin-core",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "Lightweight, data-focused HTML/CSS admin framework built with PureCSS foundation and comprehensive component system",
5
5
  "style": "dist/css/main.css",
6
6
  "exports": {
@@ -16,7 +16,6 @@
16
16
  "src/scss/",
17
17
  "schemas/",
18
18
  "scripts/pack-theme.js",
19
- "scripts/download-themes.js",
20
19
  "snippets/",
21
20
  "README.md",
22
21
  "LICENSE"
@@ -40,9 +39,6 @@
40
39
  "components",
41
40
  "ui-kit"
42
41
  ],
43
- "bin": {
44
- "download-themes": "./scripts/download-themes.js"
45
- },
46
42
  "author": "KeenMate",
47
43
  "license": "MIT",
48
44
  "peerDependencies": {
@@ -144,8 +144,9 @@
144
144
  margin: 0 0 $spacing-xs 0;
145
145
  font-size: $font-size-lg;
146
146
  font-weight: $font-weight-semibold;
147
- color: var(--pa-text-color-1);
148
- // Truncate long names
147
+ // Header bg can be dark/light/colored per theme — use the same var
148
+ // the header itself uses for the user's name so contrast is guaranteed.
149
+ color: var(--pa-header-profile-name-color);
149
150
  overflow: hidden;
150
151
  text-overflow: ellipsis;
151
152
  white-space: nowrap;
@@ -154,7 +155,8 @@
154
155
  &__email {
155
156
  margin: 0 0 $spacing-sm 0;
156
157
  font-size: $font-size-sm;
157
- color: var(--pa-text-color-2);
158
+ color: var(--pa-header-profile-name-color);
159
+ opacity: 0.75;
158
160
  overflow: hidden;
159
161
  text-overflow: ellipsis;
160
162
  white-space: nowrap;
@@ -163,8 +165,11 @@
163
165
  &__role {
164
166
  display: inline-block;
165
167
  padding: $btn-padding-v $btn-padding-h;
168
+ // Tinted bg derived from the header's name color so it reads on any
169
+ // header — dark or light. Fallback kept for older browsers.
166
170
  background-color: var(--pa-accent-light);
167
- color: var(--pa-accent);
171
+ background-color: color-mix(in srgb, var(--pa-header-profile-name-color) 15%, transparent);
172
+ color: var(--pa-header-profile-name-color);
168
173
  font-size: $font-size-xs;
169
174
  font-weight: $font-weight-medium;
170
175
  border-radius: var(--pa-border-radius);
@@ -283,17 +288,22 @@
283
288
  margin-bottom: 0;
284
289
  }
285
290
 
291
+ // Icons inherit currentColor — use the header's name color (which is
292
+ // theme-guaranteed to contrast with header-bg) with opacity for the
293
+ // inactive state instead of --pa-header-text-secondary, which some
294
+ // themes don't re-tone for colored headers.
286
295
  .pa-tabs__item {
287
- color: var(--pa-header-text-secondary);
296
+ color: var(--pa-header-profile-name-color);
297
+ opacity: 0.6;
288
298
  border-bottom-color: transparent;
289
299
 
290
300
  &:hover {
291
- color: var(--pa-header-text);
292
- background-color: var(--pa-accent-light);
301
+ opacity: 0.85;
302
+ background-color: color-mix(in srgb, var(--pa-header-profile-name-color) 10%, transparent);
293
303
  }
294
304
 
295
305
  &--active {
296
- color: var(--pa-header-text);
306
+ opacity: 1;
297
307
  border-bottom-color: var(--pa-accent);
298
308
  }
299
309
  }
@@ -161,35 +161,35 @@
161
161
  white-space: nowrap;
162
162
  }
163
163
 
164
- // Color variants
164
+ // Color variants — use CSS vars so theme overrides apply at runtime
165
165
  &.pa-stat--primary {
166
- background-color: $accent-color;
167
- color: $modal-content-bg;
166
+ background-color: var(--pa-accent);
167
+ color: var(--pa-btn-primary-text);
168
168
  }
169
169
 
170
170
  &.pa-stat--success {
171
- background-color: $success-bg;
172
- color: $btn-success-text;
171
+ background-color: var(--pa-success-bg);
172
+ color: var(--pa-btn-success-text);
173
173
  }
174
174
 
175
175
  &.pa-stat--info {
176
- background-color: $info-bg;
177
- color: $btn-info-text;
176
+ background-color: var(--pa-info-bg);
177
+ color: var(--pa-btn-info-text);
178
178
  }
179
179
 
180
180
  &.pa-stat--warning {
181
- background-color: $warning-bg;
182
- color: $btn-warning-color;
181
+ background-color: var(--pa-warning-bg);
182
+ color: var(--pa-btn-warning-text);
183
183
  }
184
184
 
185
185
  &.pa-stat--danger {
186
- background-color: $danger-bg;
187
- color: $btn-danger-text;
186
+ background-color: var(--pa-danger-bg);
187
+ color: var(--pa-btn-danger-text);
188
188
  }
189
189
 
190
190
  &.pa-stat--secondary {
191
191
  background-color: var(--pa-text-color-2);
192
- color: $modal-content-bg;
192
+ color: var(--pa-btn-primary-text);
193
193
  }
194
194
  }
195
195
  }
@@ -25,6 +25,31 @@ html.font-size-xlarge {
25
25
  font-size: 12px; // ~19px body text (12 * 1.6 = 19.2px)
26
26
  }
27
27
 
28
+ // Responsive font size classes - Apply to <html> element for automatic mobile scaling
29
+ // Desktop size applies above $mobile-breakpoint, mobile size applies at or below
30
+ // Usage: <html class="pa-font-base-10 pa-font-mobile-12">
31
+ // Shorthand: <html class="pa-font-responsive"> (10px desktop, 12px mobile)
32
+
33
+ $_font-base-sizes: (9, 10, 11, 12);
34
+
35
+ @each $size in $_font-base-sizes {
36
+ html.pa-font-base-#{$size} {
37
+ font-size: #{$size}px;
38
+ }
39
+ }
40
+
41
+ @media (max-width: $mobile-breakpoint) {
42
+ @each $size in $_font-base-sizes {
43
+ html.pa-font-mobile-#{$size} {
44
+ font-size: #{$size}px;
45
+ }
46
+ }
47
+
48
+ html.pa-font-responsive {
49
+ font-size: 12px;
50
+ }
51
+ }
52
+
28
53
  // Font family utilities
29
54
  // Only for overriding the theme's default font
30
55
  .font-family-serif {
@@ -1,351 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Download themes from pureadmin.io
5
- *
6
- * Reads themes.json + .themes.json (local override) and downloads
7
- * themes that don't have a local "path" set.
8
- *
9
- * Format:
10
- * {
11
- * "themes": {
12
- * "audi": {}, // download from pureadmin.io
13
- * "corporate": { "path": "../my-themes/corp" }, // use local path
14
- * "custom": { "url": "https://..." } // download from custom URL
15
- * }
16
- * }
17
- *
18
- * Downloaded themes are saved to ./themes/{name}/dist/{name}.css
19
- */
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
- const https = require('https');
24
- const http = require('http');
25
-
26
- const BUNDLE_URL = 'https://pureadmin.io/api/bundle';
27
- const PROJECT_ROOT = process.cwd();
28
- const THEMES_DIR = path.join(PROJECT_ROOT, 'themes');
29
- const THEMES_CONFIG = path.join(PROJECT_ROOT, 'themes.json');
30
- const THEMES_LOCAL = path.join(PROJECT_ROOT, '.themes.json');
31
-
32
- function download(url) {
33
- return new Promise((resolve, reject) => {
34
- const client = url.startsWith('https') ? https : http;
35
-
36
- client.get(url, (res) => {
37
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
38
- return download(res.headers.location).then(resolve).catch(reject);
39
- }
40
-
41
- if (res.statusCode !== 200) {
42
- reject(new Error(`HTTP ${res.statusCode} for ${url}`));
43
- return;
44
- }
45
-
46
- const chunks = [];
47
- res.on('data', chunk => chunks.push(chunk));
48
- res.on('end', () => resolve(Buffer.concat(chunks)));
49
- res.on('error', reject);
50
- }).on('error', reject);
51
- });
52
- }
53
-
54
- function extractZip(buffer, destDir) {
55
- const tmpFile = path.join(destDir, '..', '_tmp_theme.zip');
56
- if (!fs.existsSync(path.dirname(tmpFile))) {
57
- fs.mkdirSync(path.dirname(tmpFile), { recursive: true });
58
- }
59
- fs.writeFileSync(tmpFile, buffer);
60
-
61
- const { execSync } = require('child_process');
62
- try {
63
- execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: 'ignore' });
64
- } catch (e) {
65
- try {
66
- execSync(`powershell -Command "Expand-Archive -Force -Path '${tmpFile}' -DestinationPath '${destDir}'"`, { stdio: 'ignore' });
67
- } catch (e2) {
68
- fs.unlinkSync(tmpFile);
69
- throw new Error('Could not extract zip. Install unzip or use PowerShell.');
70
- }
71
- }
72
- fs.unlinkSync(tmpFile);
73
- }
74
-
75
- function findThemeCss(dir, themeName) {
76
- const entries = fs.readdirSync(dir, { withFileTypes: true });
77
-
78
- for (const entry of entries) {
79
- const fullPath = path.join(dir, entry.name);
80
-
81
- if (entry.isFile() && entry.name === `${themeName}.css`) {
82
- return fullPath;
83
- }
84
-
85
- if (entry.isDirectory()) {
86
- const found = findThemeCss(fullPath, themeName);
87
- if (found) return found;
88
- }
89
- }
90
-
91
- return null;
92
- }
93
-
94
- function getThemeVersion(themeDir) {
95
- const manifestPath = path.join(themeDir, 'theme.json');
96
- if (fs.existsSync(manifestPath)) {
97
- try {
98
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
99
- return manifest.version || null;
100
- } catch (e) {
101
- return null;
102
- }
103
- }
104
- return null;
105
- }
106
-
107
- function copyDir(src, dest) {
108
- if (!fs.existsSync(dest)) {
109
- fs.mkdirSync(dest, { recursive: true });
110
- }
111
-
112
- const entries = fs.readdirSync(src, { withFileTypes: true });
113
- for (const entry of entries) {
114
- const srcPath = path.join(src, entry.name);
115
- const destPath = path.join(dest, entry.name);
116
-
117
- if (entry.isDirectory()) {
118
- copyDir(srcPath, destPath);
119
- } else {
120
- fs.copyFileSync(srcPath, destPath);
121
- }
122
- }
123
- }
124
-
125
- async function main() {
126
- // Load themes.json (base) + .themes.json (local overrides)
127
- let themes = {};
128
-
129
- if (fs.existsSync(THEMES_CONFIG)) {
130
- const base = JSON.parse(fs.readFileSync(THEMES_CONFIG, 'utf-8'));
131
- themes = { ...themes, ...(base.themes || {}) };
132
- console.log(`Loaded themes.json (${Object.keys(base.themes || {}).length} theme(s))`);
133
- }
134
-
135
- if (fs.existsSync(THEMES_LOCAL)) {
136
- const local = JSON.parse(fs.readFileSync(THEMES_LOCAL, 'utf-8'));
137
- themes = { ...themes, ...(local.themes || {}) };
138
- console.log(`Loaded .themes.json (${Object.keys(local.themes || {}).length} override(s))`);
139
- }
140
-
141
- if (Object.keys(themes).length === 0) {
142
- console.error('No themes found. Create themes.json or .themes.json, e.g.:');
143
- console.error(' { "themes": { "audi": {}, "corporate": {} } }');
144
- process.exit(1);
145
- }
146
-
147
- console.log('');
148
-
149
- // Ensure themes directory exists
150
- if (!fs.existsSync(THEMES_DIR)) {
151
- fs.mkdirSync(THEMES_DIR, { recursive: true });
152
- }
153
-
154
- // Separate themes by type
155
- const bundleThemes = []; // No path, no url — download from bundle
156
- const urlThemes = []; // Custom url
157
- const localThemes = []; // Has path — skip
158
-
159
- for (const [name, config] of Object.entries(themes)) {
160
- const cfg = config || {};
161
- if (cfg.path) {
162
- localThemes.push(name);
163
- } else if (cfg.url) {
164
- urlThemes.push({ name, url: cfg.url });
165
- } else {
166
- bundleThemes.push(name);
167
- }
168
- }
169
-
170
- let downloaded = 0;
171
-
172
- // Download named themes via bundle (single request)
173
- if (bundleThemes.length > 0) {
174
- process.stdout.write(`Fetching theme bundle from pureadmin.io (${bundleThemes.length} theme(s))...`);
175
-
176
- try {
177
- const data = await download(BUNDLE_URL);
178
- console.log(` ${(data.length / 1024).toFixed(0)}KB`);
179
-
180
- // Extract bundle to temp dir
181
- const tmpDir = path.join(THEMES_DIR, '_bundle_tmp');
182
- if (fs.existsSync(tmpDir)) {
183
- fs.rmSync(tmpDir, { recursive: true });
184
- }
185
- fs.mkdirSync(tmpDir, { recursive: true });
186
- extractZip(data, tmpDir);
187
-
188
- // Find and copy requested themes
189
- for (const name of bundleThemes) {
190
- const cssFile = findThemeCss(tmpDir, name);
191
-
192
- if (cssFile) {
193
- const themeDestDir = path.join(THEMES_DIR, name);
194
- const distDir = path.join(themeDestDir, 'dist');
195
- if (!fs.existsSync(distDir)) {
196
- fs.mkdirSync(distDir, { recursive: true });
197
- }
198
- fs.copyFileSync(cssFile, path.join(distDir, `${name}.css`));
199
-
200
- // Copy assets dir if it exists (fonts etc.)
201
- const assetsDir = path.join(path.dirname(cssFile), 'assets');
202
- if (fs.existsSync(assetsDir)) {
203
- copyDir(assetsDir, path.join(distDir, 'assets'));
204
- }
205
-
206
- // Copy theme.json manifest if it exists
207
- const themeJsonSrc = path.join(path.dirname(cssFile), '..', 'theme.json');
208
- if (fs.existsSync(themeJsonSrc)) {
209
- fs.copyFileSync(themeJsonSrc, path.join(themeDestDir, 'theme.json'));
210
- }
211
-
212
- // Read version from theme.json
213
- const version = getThemeVersion(themeDestDir);
214
- console.log(` ${name}: extracted${version ? ` (v${version})` : ''}`);
215
- downloaded++;
216
- } else {
217
- console.log(` ${name}: NOT FOUND in bundle`);
218
- }
219
- }
220
-
221
- // Clean up temp dir
222
- fs.rmSync(tmpDir, { recursive: true });
223
- } catch (err) {
224
- console.log(` FAILED: ${err.message}`);
225
- }
226
- }
227
-
228
- // Download URL themes individually
229
- for (const { name, url } of urlThemes) {
230
- process.stdout.write(` ${name}: downloading from ${url}...`);
231
-
232
- try {
233
- const data = await download(url);
234
- const themeDestDir = path.join(THEMES_DIR, name);
235
-
236
- if (data[0] === 0x50 && data[1] === 0x4B) {
237
- if (!fs.existsSync(themeDestDir)) {
238
- fs.mkdirSync(themeDestDir, { recursive: true });
239
- }
240
- extractZip(data, themeDestDir);
241
- } else {
242
- const distDir = path.join(themeDestDir, 'dist');
243
- if (!fs.existsSync(distDir)) {
244
- fs.mkdirSync(distDir, { recursive: true });
245
- }
246
- fs.writeFileSync(path.join(distDir, `${name}.css`), data);
247
- }
248
-
249
- console.log(' done');
250
- downloaded++;
251
- } catch (err) {
252
- console.log(` FAILED: ${err.message}`);
253
- }
254
- }
255
-
256
- // Report skipped
257
- for (const name of localThemes) {
258
- console.log(` ${name}: local path (${themes[name].path}), skipping`);
259
- }
260
-
261
- console.log(`\nDownloaded ${downloaded} theme(s), skipped ${localThemes.length} local path(s)`);
262
-
263
- // Check compatibility with installed core version
264
- checkCoreCompatibility();
265
- }
266
-
267
- /**
268
- * Check if downloaded themes are compatible with the installed core version
269
- */
270
- function checkCoreCompatibility() {
271
- // Find installed core version
272
- const corePkgPaths = [
273
- path.join(PROJECT_ROOT, 'node_modules', '@keenmate', 'pure-admin-core', 'package.json'),
274
- path.join(PROJECT_ROOT, '..', 'packages', 'core', 'package.json') // workspace
275
- ];
276
-
277
- let coreVersion = null;
278
- for (const p of corePkgPaths) {
279
- if (fs.existsSync(p)) {
280
- try {
281
- const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
282
- coreVersion = pkg.version;
283
- break;
284
- } catch (e) {}
285
- }
286
- }
287
-
288
- if (!coreVersion) return;
289
-
290
- // Check each downloaded theme
291
- const warnings = [];
292
- const themeDirs = fs.readdirSync(THEMES_DIR, { withFileTypes: true })
293
- .filter(d => d.isDirectory())
294
- .map(d => d.name);
295
-
296
- for (const name of themeDirs) {
297
- const manifestPath = path.join(THEMES_DIR, name, 'theme.json');
298
- if (!fs.existsSync(manifestPath)) continue;
299
-
300
- try {
301
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
302
- const requiredCore = manifest.dependencies && manifest.dependencies.core;
303
- if (!requiredCore) continue;
304
-
305
- if (!satisfiesSemver(coreVersion, requiredCore)) {
306
- warnings.push(` ${name} (v${manifest.version}) requires core ${requiredCore}, installed: ${coreVersion}`);
307
- }
308
- } catch (e) {}
309
- }
310
-
311
- if (warnings.length > 0) {
312
- console.log('\n⚠ Compatibility warnings:');
313
- warnings.forEach(w => console.log(w));
314
- console.log('\nConsider updating @keenmate/pure-admin-core');
315
- } else if (themeDirs.length > 0) {
316
- console.log(`\nAll themes compatible with core v${coreVersion}`);
317
- }
318
- }
319
-
320
- /**
321
- * Simple semver satisfaction check for ^major.minor.patch ranges
322
- */
323
- function satisfiesSemver(version, range) {
324
- // Parse version
325
- const v = version.replace(/^v/, '').split('.').map(Number);
326
-
327
- // Parse range (supports ^x.y.z and x.y.z)
328
- const caret = range.startsWith('^');
329
- const r = range.replace(/^[\^~]/, '').split('.').map(Number);
330
-
331
- if (caret) {
332
- // ^2.0.0 means >=2.0.0 <3.0.0
333
- // ^0.2.0 means >=0.2.0 <0.3.0
334
- if (r[0] > 0) {
335
- return v[0] === r[0] && (v[1] > r[1] || (v[1] === r[1] && v[2] >= r[2]));
336
- } else if (r[1] > 0) {
337
- return v[0] === 0 && v[1] === r[1] && v[2] >= r[2];
338
- } else {
339
- return v[0] === 0 && v[1] === 0 && v[2] === r[2];
340
- }
341
- }
342
-
343
- // Exact match
344
- return v[0] === r[0] && v[1] === r[1] && v[2] === r[2];
345
- }
346
-
347
- console.log('Downloading themes...\n');
348
- main().catch(err => {
349
- console.error('Fatal error:', err.message);
350
- process.exit(1);
351
- });