@keenmate/pure-admin-core 1.5.1 → 2.0.1

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,397 @@
1
+ #!/usr/bin/env node
2
+
3
+ // =============================================================================
4
+ // pack-theme.js — Package a Pure Admin theme into a distributable zip
5
+ // =============================================================================
6
+ //
7
+ // Usage:
8
+ // node pack-theme.js <theme-dir> [--output <dir>]
9
+ //
10
+ // Example:
11
+ // node pack-theme.js ../pure-admin-cafeindustrial-theme/
12
+ // node pack-theme.js ./my-theme --output ./releases/
13
+ //
14
+ // The script:
15
+ // 1. Reads and validates theme.json from the theme directory
16
+ // 2. Verifies CSS file exists (or compiles SCSS if missing)
17
+ // 3. Generates a README.md with usage instructions
18
+ // 4. Packages everything into pure-admin-theme-{id}-{version}.zip
19
+ //
20
+ // Zip contents:
21
+ // pure-admin-theme-{id}-{version}.zip
22
+ // ├── theme.json
23
+ // ├── css/{id}.css
24
+ // ├── scss/{id}.scss
25
+ // ├── preview/thumbnail.png (if exists)
26
+ // └── README.md
27
+ // =============================================================================
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const { execSync } = require('child_process');
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Parse arguments
35
+ // ---------------------------------------------------------------------------
36
+ const args = process.argv.slice(2);
37
+ let themeDir = null;
38
+ let outputDir = null;
39
+
40
+ for (let i = 0; i < args.length; i++) {
41
+ if (args[i] === '--output' && args[i + 1]) {
42
+ outputDir = args[++i];
43
+ } else if (args[i] === '--help' || args[i] === '-h') {
44
+ console.log(`
45
+ Usage: node pack-theme.js <theme-dir> [--output <dir>]
46
+
47
+ Options:
48
+ --output <dir> Output directory for the zip (default: theme-dir/dist/)
49
+ --help, -h Show this help message
50
+
51
+ Examples:
52
+ node pack-theme.js ../pure-admin-cafeindustrial-theme/
53
+ node pack-theme.js ./my-theme --output ./releases/
54
+ `);
55
+ process.exit(0);
56
+ } else if (!themeDir) {
57
+ themeDir = args[i];
58
+ }
59
+ }
60
+
61
+ if (!themeDir) {
62
+ console.error('Error: No theme directory specified.');
63
+ console.error('Usage: node pack-theme.js <theme-dir> [--output <dir>]');
64
+ process.exit(1);
65
+ }
66
+
67
+ // Resolve paths
68
+ themeDir = path.resolve(themeDir);
69
+ const coreDir = path.resolve(__dirname, '..');
70
+
71
+ if (!fs.existsSync(themeDir)) {
72
+ console.error(`Error: Theme directory not found: ${themeDir}`);
73
+ process.exit(1);
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // 1. Read and validate theme.json
78
+ // ---------------------------------------------------------------------------
79
+ const themeJsonPath = path.join(themeDir, 'theme.json');
80
+ if (!fs.existsSync(themeJsonPath)) {
81
+ console.error(`Error: theme.json not found in ${themeDir}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ let theme;
86
+ try {
87
+ theme = JSON.parse(fs.readFileSync(themeJsonPath, 'utf-8'));
88
+ } catch (err) {
89
+ console.error(`Error: Invalid JSON in theme.json: ${err.message}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ // Validate required fields
94
+ const requiredFields = ['name', 'id', 'version', 'modes', 'exports'];
95
+ const missing = requiredFields.filter(f => !theme[f]);
96
+ if (missing.length > 0) {
97
+ console.error(`Error: theme.json is missing required fields: ${missing.join(', ')}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ // Validate id format
102
+ if (!/^[a-z][a-z0-9-]*$/.test(theme.id)) {
103
+ console.error(`Error: theme.id "${theme.id}" must be lowercase alphanumeric with hyphens (e.g. "cafeindustrial")`);
104
+ process.exit(1);
105
+ }
106
+
107
+ // Validate version format
108
+ if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(theme.version)) {
109
+ console.error(`Error: theme.version "${theme.version}" must be semver (e.g. "1.0.0")`);
110
+ process.exit(1);
111
+ }
112
+
113
+ console.log(`Packaging theme: ${theme.name} v${theme.version} (${theme.id})`);
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // 2. Locate or compile CSS
117
+ // ---------------------------------------------------------------------------
118
+ let cssSourcePath = null;
119
+
120
+ // Check exports.css path first
121
+ if (theme.exports && theme.exports.css) {
122
+ const exportedCss = path.resolve(themeDir, theme.exports.css);
123
+ if (fs.existsSync(exportedCss)) {
124
+ cssSourcePath = exportedCss;
125
+ }
126
+ }
127
+
128
+ // Also check common locations
129
+ if (!cssSourcePath) {
130
+ const candidates = [
131
+ path.join(themeDir, 'dist', `${theme.id}.css`),
132
+ path.join(themeDir, 'dist', 'css', `${theme.id}.css`),
133
+ path.join(themeDir, 'css', `${theme.id}.css`),
134
+ ];
135
+ for (const candidate of candidates) {
136
+ if (fs.existsSync(candidate)) {
137
+ cssSourcePath = candidate;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+
143
+ // If no CSS found, try to compile from SCSS
144
+ if (!cssSourcePath) {
145
+ console.log('No compiled CSS found, attempting to compile from SCSS...');
146
+
147
+ let scssPath = null;
148
+ if (theme.exports && theme.exports.scss) {
149
+ const exportedScss = path.resolve(themeDir, theme.exports.scss);
150
+ if (fs.existsSync(exportedScss)) {
151
+ scssPath = exportedScss;
152
+ }
153
+ }
154
+ if (!scssPath) {
155
+ scssPath = path.join(themeDir, 'src', 'scss', `${theme.id}.scss`);
156
+ }
157
+
158
+ if (!fs.existsSync(scssPath)) {
159
+ console.error(`Error: No CSS or SCSS source found for theme "${theme.id}"`);
160
+ process.exit(1);
161
+ }
162
+
163
+ // Compile SCSS
164
+ const tempCssDir = path.join(themeDir, 'dist');
165
+ if (!fs.existsSync(tempCssDir)) {
166
+ fs.mkdirSync(tempCssDir, { recursive: true });
167
+ }
168
+
169
+ const tempCssPath = path.join(tempCssDir, `${theme.id}.css`);
170
+
171
+ // Build load paths for sass
172
+ // 1. node_modules at workspace root (for @keenmate/pure-admin-core/... imports)
173
+ // 2. core scss dir (for legacy bare imports)
174
+ const loadPaths = [];
175
+
176
+ // Check for node_modules in various locations
177
+ const nodeModulesCandidates = [
178
+ path.join(themeDir, 'node_modules'),
179
+ path.join(themeDir, '..', 'node_modules'),
180
+ path.join(coreDir, '..', '..', 'node_modules'),
181
+ ];
182
+ for (const nm of nodeModulesCandidates) {
183
+ if (fs.existsSync(nm)) {
184
+ loadPaths.push(nm);
185
+ }
186
+ }
187
+
188
+ // Also add core scss dir for bare imports
189
+ loadPaths.push(path.join(coreDir, 'src', 'scss'));
190
+
191
+ const loadPathArgs = loadPaths.map(p => `--load-path="${p}"`).join(' ');
192
+ const sassCmd = `sass "${scssPath}" "${tempCssPath}" --no-source-map --silence-deprecation=import ${loadPathArgs}`;
193
+
194
+ try {
195
+ console.log(` Compiling: ${path.basename(scssPath)}`);
196
+ execSync(sassCmd, { stdio: 'pipe' });
197
+ cssSourcePath = tempCssPath;
198
+ console.log(' Compilation successful.');
199
+ } catch (err) {
200
+ console.error(`Error: SCSS compilation failed:`);
201
+ console.error(err.stderr ? err.stderr.toString() : err.message);
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ console.log(` CSS: ${path.relative(themeDir, cssSourcePath)}`);
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // 3. Locate SCSS source
210
+ // ---------------------------------------------------------------------------
211
+ let scssSourcePath = null;
212
+ if (theme.exports && theme.exports.scss) {
213
+ const exportedScss = path.resolve(themeDir, theme.exports.scss);
214
+ if (fs.existsSync(exportedScss)) {
215
+ scssSourcePath = exportedScss;
216
+ }
217
+ }
218
+ if (!scssSourcePath) {
219
+ const candidate = path.join(themeDir, 'src', 'scss', `${theme.id}.scss`);
220
+ if (fs.existsSync(candidate)) {
221
+ scssSourcePath = candidate;
222
+ }
223
+ }
224
+
225
+ if (scssSourcePath) {
226
+ console.log(` SCSS: ${path.relative(themeDir, scssSourcePath)}`);
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // 4. Locate preview thumbnail
231
+ // ---------------------------------------------------------------------------
232
+ let thumbnailPath = null;
233
+ const thumbnailCandidates = [
234
+ theme.preview && theme.preview.thumbnail ? path.resolve(themeDir, theme.preview.thumbnail) : null,
235
+ path.join(themeDir, 'preview', 'thumbnail.png'),
236
+ path.join(themeDir, 'preview', 'thumbnail.jpg'),
237
+ ].filter(Boolean);
238
+
239
+ for (const candidate of thumbnailCandidates) {
240
+ if (fs.existsSync(candidate)) {
241
+ thumbnailPath = candidate;
242
+ break;
243
+ }
244
+ }
245
+
246
+ if (thumbnailPath) {
247
+ console.log(` Preview: ${path.relative(themeDir, thumbnailPath)}`);
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // 5. Generate README.md
252
+ // ---------------------------------------------------------------------------
253
+ const modesText = theme.modes && theme.modes.supported
254
+ ? theme.modes.supported.join(', ')
255
+ : 'light';
256
+
257
+ const tagsText = theme.tags && theme.tags.length > 0
258
+ ? theme.tags.join(', ')
259
+ : '';
260
+
261
+ const coreVersionText = theme.coreVersion || (theme.dependencies && theme.dependencies.core) || '>=1.5.0';
262
+
263
+ const readme = `# ${theme.name}
264
+
265
+ ${theme.description || ''}
266
+
267
+ - **Version:** ${theme.version}
268
+ - **Author:** ${theme.author || 'Unknown'}
269
+ - **License:** ${theme.license || 'MIT'}
270
+ - **Modes:** ${modesText}
271
+ - **Core Version:** ${coreVersionText}
272
+ ${tagsText ? `- **Tags:** ${tagsText}` : ''}
273
+
274
+ ## Quick Start — CSS Only
275
+
276
+ Drop the compiled CSS file into your project:
277
+
278
+ \`\`\`html
279
+ <link rel="stylesheet" href="css/${theme.id}.css">
280
+ \`\`\`
281
+
282
+ No build tools required. The CSS is fully self-contained.
283
+
284
+ ## Quick Start — SCSS Customization
285
+
286
+ If you want to customize theme variables before compiling:
287
+
288
+ 1. Install the core package:
289
+ \`\`\`bash
290
+ npm install @keenmate/pure-admin-core
291
+ \`\`\`
292
+
293
+ 2. Compile with sass:
294
+ \`\`\`bash
295
+ sass scss/${theme.id}.scss output.css \\
296
+ --load-path=node_modules \\
297
+ --silence-deprecation=import
298
+ \`\`\`
299
+
300
+ 3. Or import in your own SCSS and override variables before the import.
301
+
302
+ ## Mode Switching
303
+ ${theme.modes && theme.modes.supported && theme.modes.supported.length > 1
304
+ ? `This theme supports ${modesText} modes. Add the mode class to toggle:
305
+
306
+ \`\`\`html
307
+ <body class="pa-mode-dark"> <!-- dark mode -->
308
+ <body class="pa-mode-light"> <!-- light mode -->
309
+ \`\`\``
310
+ : `This theme supports ${modesText} mode.`}
311
+
312
+ ## More Information
313
+
314
+ - Pure Admin documentation: https://pure-admin.keenmate.dev
315
+ - Theme gallery: https://pure-theme-park.keenmate.dev
316
+ ${theme.homepage ? `- Theme homepage: ${theme.homepage}` : ''}
317
+
318
+ ---
319
+ *Generated by pure-admin-core pack-theme*
320
+ `;
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // 6. Create the zip
324
+ // ---------------------------------------------------------------------------
325
+ const zipName = `pure-admin-theme-${theme.id}-${theme.version}.zip`;
326
+
327
+ if (!outputDir) {
328
+ outputDir = path.join(themeDir, 'dist');
329
+ }
330
+ outputDir = path.resolve(outputDir);
331
+
332
+ if (!fs.existsSync(outputDir)) {
333
+ fs.mkdirSync(outputDir, { recursive: true });
334
+ }
335
+
336
+ const zipPath = path.join(outputDir, zipName);
337
+
338
+ // Try to load archiver, fall back to manual zip creation
339
+ let archiver;
340
+ try {
341
+ archiver = require('archiver');
342
+ } catch {
343
+ console.error('Error: "archiver" package is required but not installed.');
344
+ console.error('Install it with: npm install archiver --save-dev');
345
+ process.exit(1);
346
+ }
347
+
348
+ const output = fs.createWriteStream(zipPath);
349
+ const archive = archiver('zip', { zlib: { level: 9 } });
350
+
351
+ archive.on('error', (err) => {
352
+ console.error(`Error creating zip: ${err.message}`);
353
+ process.exit(1);
354
+ });
355
+
356
+ archive.on('warning', (err) => {
357
+ if (err.code === 'ENOENT') {
358
+ console.warn(`Warning: ${err.message}`);
359
+ } else {
360
+ throw err;
361
+ }
362
+ });
363
+
364
+ output.on('close', () => {
365
+ const sizeKB = (archive.pointer() / 1024).toFixed(1);
366
+ console.log(`\nCreated: ${zipPath}`);
367
+ console.log(`Size: ${sizeKB} KB`);
368
+ console.log('\nZip contents:');
369
+ console.log(` theme.json`);
370
+ console.log(` css/${theme.id}.css`);
371
+ if (scssSourcePath) console.log(` scss/${theme.id}.scss`);
372
+ if (thumbnailPath) console.log(` preview/${path.basename(thumbnailPath)}`);
373
+ console.log(` README.md`);
374
+ });
375
+
376
+ archive.pipe(output);
377
+
378
+ // Add theme.json
379
+ archive.file(themeJsonPath, { name: 'theme.json' });
380
+
381
+ // Add CSS
382
+ archive.file(cssSourcePath, { name: `css/${theme.id}.css` });
383
+
384
+ // Add SCSS (if available)
385
+ if (scssSourcePath) {
386
+ archive.file(scssSourcePath, { name: `scss/${theme.id}.scss` });
387
+ }
388
+
389
+ // Add preview thumbnail (if available)
390
+ if (thumbnailPath) {
391
+ archive.file(thumbnailPath, { name: `preview/${path.basename(thumbnailPath)}` });
392
+ }
393
+
394
+ // Add generated README
395
+ archive.append(readme, { name: 'README.md' });
396
+
397
+ archive.finalize();
@@ -111,5 +111,8 @@
111
111
  // Data Display (read-only fields)
112
112
  @use 'core-components/data-display' as *;
113
113
 
114
+ // Data Visualization (progress bars, rings, gauges, heatmaps, sparklines)
115
+ @use 'core-components/data-viz' as *;
116
+
114
117
  // Utility classes and helpers
115
118
  @use 'core-components/utilities' as *;
@@ -214,6 +214,8 @@
214
214
  &--loading {
215
215
  position: relative;
216
216
  pointer-events: none;
217
+ -webkit-text-fill-color: transparent; // Hide text but preserve layout and currentColor for spinner
218
+ transition: none;
217
219
 
218
220
  .pa-btn__spinner {
219
221
  position: absolute;
@@ -32,7 +32,8 @@
32
32
  gap: $spacing-base; // Gap between header elements (title, description, actions)
33
33
  min-width: 0; // Enable text truncation
34
34
 
35
- // Reset margins/paddings for all native elements
35
+ // Reset margins/paddings/borders for all native elements
36
+ // (border-bottom reset prevents .pa-section h3 rule from bleeding in)
36
37
  h1,
37
38
  h2,
38
39
  h3,
@@ -49,6 +50,7 @@
49
50
  fieldset {
50
51
  margin: 0;
51
52
  padding: 0;
53
+ border-bottom: none;
52
54
  }
53
55
 
54
56
  // Specific heading styles
@@ -96,6 +98,63 @@
96
98
  flex-shrink: 0;
97
99
  }
98
100
 
101
+ // Underline modifier - accent border under heading inside card header
102
+ &--underlined {
103
+ h1, h2, h3, h4, h5, h6 {
104
+ border-bottom: $border-width-medium solid var(--pa-accent);
105
+ padding-bottom: $spacing-sm;
106
+ }
107
+
108
+ // Semantic color variants
109
+ &.pa-card__header--underline-success h1,
110
+ &.pa-card__header--underline-success h2,
111
+ &.pa-card__header--underline-success h3,
112
+ &.pa-card__header--underline-success h4,
113
+ &.pa-card__header--underline-success h5,
114
+ &.pa-card__header--underline-success h6 {
115
+ border-bottom-color: var(--pa-success-bg);
116
+ }
117
+
118
+ &.pa-card__header--underline-warning h1,
119
+ &.pa-card__header--underline-warning h2,
120
+ &.pa-card__header--underline-warning h3,
121
+ &.pa-card__header--underline-warning h4,
122
+ &.pa-card__header--underline-warning h5,
123
+ &.pa-card__header--underline-warning h6 {
124
+ border-bottom-color: var(--pa-warning-bg);
125
+ }
126
+
127
+ &.pa-card__header--underline-danger h1,
128
+ &.pa-card__header--underline-danger h2,
129
+ &.pa-card__header--underline-danger h3,
130
+ &.pa-card__header--underline-danger h4,
131
+ &.pa-card__header--underline-danger h5,
132
+ &.pa-card__header--underline-danger h6 {
133
+ border-bottom-color: var(--pa-danger-bg);
134
+ }
135
+
136
+ &.pa-card__header--underline-info h1,
137
+ &.pa-card__header--underline-info h2,
138
+ &.pa-card__header--underline-info h3,
139
+ &.pa-card__header--underline-info h4,
140
+ &.pa-card__header--underline-info h5,
141
+ &.pa-card__header--underline-info h6 {
142
+ border-bottom-color: var(--pa-info-bg);
143
+ }
144
+
145
+ // Theme color slots (1-9)
146
+ @for $i from 1 through 9 {
147
+ &.pa-card__header--underline-color-#{$i} h1,
148
+ &.pa-card__header--underline-color-#{$i} h2,
149
+ &.pa-card__header--underline-color-#{$i} h3,
150
+ &.pa-card__header--underline-color-#{$i} h4,
151
+ &.pa-card__header--underline-color-#{$i} h5,
152
+ &.pa-card__header--underline-color-#{$i} h6 {
153
+ border-bottom-color: var(--pa-color-#{$i});
154
+ }
155
+ }
156
+ }
157
+
99
158
  // Wrap modifier - allow description to wrap (for mobile or long descriptions)
100
159
  &--wrap {
101
160
  flex-wrap: wrap;
@@ -277,6 +336,23 @@
277
336
  }
278
337
  }
279
338
 
339
+ // Live-data state — persistent tinted background reflecting latest change
340
+ // JS swaps the class on each data update; color stays until next update
341
+ &--live-up {
342
+ background-color: rgba($success-bg, 0.10);
343
+ transition: background-color 0.3s ease;
344
+ }
345
+
346
+ &--live-down {
347
+ background-color: rgba($danger-bg, 0.10);
348
+ transition: background-color 0.3s ease;
349
+ }
350
+
351
+ &--live-neutral {
352
+ background-color: $card-bg;
353
+ transition: background-color 0.3s ease;
354
+ }
355
+
280
356
  // Theme color variants (color-1 through color-9)
281
357
  // These use theme-customizable colors from --pa-color-* CSS variables
282
358
  @for $i from 1 through 9 {
@@ -390,7 +466,7 @@ a.pa-card {
390
466
  .pa-section {
391
467
  margin-bottom: $section-margin-v;
392
468
 
393
- h3 {
469
+ > h3 {
394
470
  color: var(--pa-text-color-1);
395
471
  margin-bottom: $spacing-base;
396
472
  border-bottom: $border-width-medium solid var(--pa-accent);