@keenmate/pure-admin-core 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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 *;
@@ -277,6 +277,23 @@
277
277
  }
278
278
  }
279
279
 
280
+ // Live-data state — persistent tinted background reflecting latest change
281
+ // JS swaps the class on each data update; color stays until next update
282
+ &--live-up {
283
+ background-color: rgba($success-bg, 0.10);
284
+ transition: background-color 0.3s ease;
285
+ }
286
+
287
+ &--live-down {
288
+ background-color: rgba($danger-bg, 0.10);
289
+ transition: background-color 0.3s ease;
290
+ }
291
+
292
+ &--live-neutral {
293
+ background-color: $card-bg;
294
+ transition: background-color 0.3s ease;
295
+ }
296
+
280
297
  // Theme color variants (color-1 through color-9)
281
298
  // These use theme-customizable colors from --pa-color-* CSS variables
282
299
  @for $i from 1 through 9 {
@@ -4,6 +4,22 @@
4
4
  ======================================== */
5
5
  @use '../variables' as *;
6
6
 
7
+ // Copy button base styles (shared across data display patterns)
8
+ @mixin _copy-btn-base {
9
+ flex-shrink: 0;
10
+ padding: $field-copy-padding;
11
+ background: transparent;
12
+ border: none;
13
+ border-radius: $border-radius;
14
+ cursor: pointer;
15
+ transition: opacity $field-copy-transition, background $field-copy-transition;
16
+
17
+ &:hover {
18
+ opacity: $field-copy-hover-opacity;
19
+ background: $field-copy-hover-bg;
20
+ }
21
+ }
22
+
7
23
  // ============================================================================
8
24
  // SINGLE FIELD (.pa-field)
9
25
  // Default: stacked (label on top, value below)
@@ -579,6 +595,65 @@
579
595
  grid-column: 2;
580
596
  }
581
597
  }
598
+
599
+ // ------------------------------------------------------------------
600
+ // COPYABLE
601
+ // ------------------------------------------------------------------
602
+
603
+ &__copy {
604
+ @include _copy-btn-base;
605
+ }
606
+
607
+ &__value--copy-btn,
608
+ &__value--copy-hover {
609
+ display: flex;
610
+ align-items: center;
611
+ gap: $spacing-sm;
612
+ }
613
+
614
+ &__value--copy-btn .pa-desc-table__copy {
615
+ opacity: $field-copy-opacity;
616
+ }
617
+
618
+ &__value--copy-hover .pa-desc-table__copy {
619
+ opacity: 0;
620
+ }
621
+
622
+ &__value--copy-hover:hover .pa-desc-table__copy {
623
+ opacity: $field-copy-opacity;
624
+
625
+ &:hover {
626
+ opacity: $field-copy-hover-opacity;
627
+ background: $field-copy-hover-bg;
628
+ }
629
+ }
630
+
631
+ &__value--copy-click {
632
+ cursor: pointer;
633
+ transition: opacity $field-copy-transition;
634
+
635
+ &:hover {
636
+ opacity: $field-copy-click-hover-opacity;
637
+ }
638
+
639
+ &::after {
640
+ content: 'Click to copy';
641
+ font-size: $font-size-2xs;
642
+ opacity: 0;
643
+ margin-inline-start: $spacing-sm;
644
+ transition: opacity $field-copy-transition;
645
+ }
646
+
647
+ &:hover::after {
648
+ opacity: $field-copy-hint-opacity;
649
+ }
650
+ }
651
+
652
+ &__value--copied::after {
653
+ content: 'Copied!' !important;
654
+ opacity: 1 !important;
655
+ color: var(--pa-color-4, #28a745);
656
+ }
582
657
  }
583
658
 
584
659
  // ============================================================================
@@ -690,6 +765,52 @@
690
765
  font-weight: $font-weight-bold;
691
766
  }
692
767
  }
768
+
769
+ // ------------------------------------------------------------------
770
+ // COPYABLE
771
+ // ------------------------------------------------------------------
772
+
773
+ &__copy {
774
+ @include _copy-btn-base;
775
+ }
776
+
777
+ &__row--copy-btn &__value,
778
+ &__row--copy-hover &__value {
779
+ display: flex;
780
+ align-items: center;
781
+ gap: $spacing-sm;
782
+ }
783
+
784
+ &__row--copy-btn .pa-prop-card__copy,
785
+ &__row--copy-hover .pa-prop-card__copy {
786
+ order: -1;
787
+ }
788
+
789
+ &__row--copy-btn .pa-prop-card__copy {
790
+ opacity: $field-copy-opacity;
791
+ }
792
+
793
+ &__row--copy-hover .pa-prop-card__copy {
794
+ opacity: 0;
795
+ }
796
+
797
+ &__row--copy-hover:hover .pa-prop-card__copy {
798
+ opacity: $field-copy-opacity;
799
+
800
+ &:hover {
801
+ opacity: $field-copy-hover-opacity;
802
+ background: $field-copy-hover-bg;
803
+ }
804
+ }
805
+
806
+ &__row--copy-click &__value {
807
+ cursor: pointer;
808
+ transition: opacity $field-copy-transition;
809
+
810
+ &:hover {
811
+ opacity: $field-copy-click-hover-opacity;
812
+ }
813
+ }
693
814
  }
694
815
 
695
816
  // ============================================================================
@@ -801,6 +922,63 @@
801
922
  display: block;
802
923
  }
803
924
  }
925
+
926
+ // ------------------------------------------------------------------
927
+ // COPYABLE
928
+ // ------------------------------------------------------------------
929
+
930
+ &__copy {
931
+ @include _copy-btn-base;
932
+ }
933
+
934
+ &__row--copy-btn &__value,
935
+ &__row--copy-hover &__value {
936
+ gap: $spacing-sm;
937
+ }
938
+
939
+ &__row--copy-btn .pa-banded__copy {
940
+ opacity: $field-copy-opacity;
941
+ }
942
+
943
+ &__row--copy-hover .pa-banded__copy {
944
+ opacity: 0;
945
+ }
946
+
947
+ &__row--copy-hover:hover .pa-banded__copy {
948
+ opacity: $field-copy-opacity;
949
+
950
+ &:hover {
951
+ opacity: $field-copy-hover-opacity;
952
+ background: $field-copy-hover-bg;
953
+ }
954
+ }
955
+
956
+ &__row--copy-click &__value {
957
+ cursor: pointer;
958
+ transition: opacity $field-copy-transition;
959
+
960
+ &:hover {
961
+ opacity: $field-copy-click-hover-opacity;
962
+ }
963
+
964
+ &::after {
965
+ content: 'Click to copy';
966
+ font-size: $font-size-2xs;
967
+ opacity: 0;
968
+ margin-inline-start: $spacing-sm;
969
+ transition: opacity $field-copy-transition;
970
+ }
971
+
972
+ &:hover::after {
973
+ opacity: $field-copy-hint-opacity;
974
+ }
975
+ }
976
+
977
+ &__row--copied &__value::after {
978
+ content: 'Copied!' !important;
979
+ opacity: 1 !important;
980
+ color: var(--pa-color-4, #28a745);
981
+ }
804
982
  }
805
983
 
806
984
  // ============================================================================
@@ -839,4 +1017,63 @@
839
1017
  color: var(--pa-text-color-1);
840
1018
  line-height: $accent-grid-value-line-height;
841
1019
  }
1020
+
1021
+ // ------------------------------------------------------------------
1022
+ // COPYABLE
1023
+ // ------------------------------------------------------------------
1024
+
1025
+ &__copy {
1026
+ @include _copy-btn-base;
1027
+ }
1028
+
1029
+ &__item--copy-btn &__value,
1030
+ &__item--copy-hover &__value {
1031
+ display: flex;
1032
+ align-items: center;
1033
+ gap: $spacing-sm;
1034
+ }
1035
+
1036
+ &__item--copy-btn .pa-accent-grid__copy {
1037
+ opacity: $field-copy-opacity;
1038
+ }
1039
+
1040
+ &__item--copy-hover .pa-accent-grid__copy {
1041
+ opacity: 0;
1042
+ }
1043
+
1044
+ &__item--copy-hover:hover .pa-accent-grid__copy {
1045
+ opacity: $field-copy-opacity;
1046
+
1047
+ &:hover {
1048
+ opacity: $field-copy-hover-opacity;
1049
+ background: $field-copy-hover-bg;
1050
+ }
1051
+ }
1052
+
1053
+ &__item--copy-click &__value {
1054
+ cursor: pointer;
1055
+ transition: opacity $field-copy-transition;
1056
+
1057
+ &:hover {
1058
+ opacity: $field-copy-click-hover-opacity;
1059
+ }
1060
+
1061
+ &::after {
1062
+ content: 'Click to copy';
1063
+ font-size: $font-size-2xs;
1064
+ opacity: 0;
1065
+ margin-inline-start: $spacing-sm;
1066
+ transition: opacity $field-copy-transition;
1067
+ }
1068
+
1069
+ &:hover::after {
1070
+ opacity: $field-copy-hint-opacity;
1071
+ }
1072
+ }
1073
+
1074
+ &__item--copied &__value::after {
1075
+ content: 'Copied!' !important;
1076
+ opacity: 1 !important;
1077
+ color: var(--pa-color-4, #28a745);
1078
+ }
842
1079
  }