@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
|
-
|
|
37
|
+
Download themes via the CLI and link the CSS:
|
|
39
38
|
|
|
40
39
|
```bash
|
|
41
|
-
npm install @keenmate/
|
|
40
|
+
npm install -g @keenmate/pureadmin
|
|
41
|
+
pureadmin themes audi
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
```html
|
|
45
|
-
<link rel="stylesheet" href="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
137
|
-
"
|
|
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:
|
|
6418
|
-
color:
|
|
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:
|
|
6422
|
-
color:
|
|
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:
|
|
6426
|
-
color:
|
|
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:
|
|
6430
|
-
color:
|
|
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:
|
|
6434
|
-
color:
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
12450
|
-
background-color: var(--pa-
|
|
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
|
-
|
|
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
|
+
"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
|
-
|
|
148
|
-
//
|
|
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-
|
|
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-
|
|
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-
|
|
296
|
+
color: var(--pa-header-profile-name-color);
|
|
297
|
+
opacity: 0.6;
|
|
288
298
|
border-bottom-color: transparent;
|
|
289
299
|
|
|
290
300
|
&:hover {
|
|
291
|
-
|
|
292
|
-
background-color: var(--pa-
|
|
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
|
-
|
|
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:
|
|
167
|
-
color:
|
|
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:
|
|
172
|
-
color:
|
|
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:
|
|
177
|
-
color:
|
|
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:
|
|
182
|
-
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:
|
|
187
|
-
color:
|
|
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:
|
|
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
|
-
});
|