@chemx/create-chemx 26.9.16-41 → 26.9.17-309
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 +4 -0
- package/assets/chemx-starter-kit-logo.gif +0 -0
- package/blueprints/_view-template.scss +24 -0
- package/blueprints/atoms/a-button.vue +43 -0
- package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
- package/blueprints/molecule-capsule/index.ts +2 -0
- package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
- package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
- package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
- package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
- package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
- package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
- package/blueprints/molecule-capsule/types.d.ts +12 -0
- package/blueprints/shims-vue.d.ts +4 -0
- package/blueprints/view-template.tsx +20 -0
- package/blueprints/workflows/chemx-audit.yml +79 -0
- package/cli/audit/autofix.js +4 -5
- package/cli/audit/pattern-detector.js +90 -3
- package/cli/audit/pattern-detector.spec.js +36 -1
- package/cli/audit/prompts.js +10 -12
- package/cli/audit/roadmap.js +5 -20
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules.js +4 -0
- package/cli/audit-preflight-git.js +54 -0
- package/cli/audit-preflight-scope.js +87 -0
- package/cli/audit-preflight.d.ts +16 -0
- package/cli/audit-preflight.js +85 -0
- package/cli/audit-preflight.spec.js +115 -0
- package/cli/audit.js +47 -22
- package/cli/generator-templates/controller.js +36 -0
- package/cli/generator-templates/hooks.js +60 -0
- package/cli/generator-templates/naming.js +14 -0
- package/cli/generator-templates/react.js +63 -0
- package/cli/generator-templates/specs.js +38 -0
- package/cli/generator-templates/styles.js +64 -0
- package/cli/generator-templates/svelte.js +66 -0
- package/cli/generator-templates/types.js +29 -0
- package/cli/generator-templates/views.js +76 -0
- package/cli/generator-templates/vue.js +62 -0
- package/cli/generator-templates.d.ts +35 -0
- package/cli/generator-templates.js +28 -480
- package/cli/generator-templates.spec.js +108 -0
- package/cli/generator.js +2 -2
- package/cli/generator.spec.js +61 -1
- package/cli/help.js +9 -0
- package/cli/index.js +44 -7
- package/cli/installer-templates.js +67 -13
- package/cli/license.js +22 -1
- package/cli/mcp/installer.js +31 -21
- package/cli/mcp/resources.js +114 -7
- package/cli/mcp/server.spec.js +34 -0
- package/cli/mcp/tools.js +14 -4
- package/cli/postinstall.js +27 -0
- package/cli/project-detector.js +20 -2
- package/cli/project-detector.spec.js +19 -0
- package/cli/reader.js +18 -4
- package/cli/scaffold.js +19 -3
- package/cli/terminal.js +36 -1
- package/docs/CHANGELOG.md +10 -0
- package/package.json +8 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Chemical X Protocol: Starter Kit
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/chemx-starter-kit-logo.gif" alt="Chemical X Starter Kit Comic Logo" width="380" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
[](https://chemicalx.xophz.com)
|
|
4
8
|
[](https://github.com/Chemical-X-Protocol/awesome-secret-sauce)
|
|
5
9
|
[](https://github.com/Chemical-X-Protocol/benchmarks)
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.view-template {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
background: #0b1329;
|
|
4
|
+
color: #e2e8f0;
|
|
5
|
+
min-height: 100vh;
|
|
6
|
+
|
|
7
|
+
&__header {
|
|
8
|
+
margin-bottom: 24px;
|
|
9
|
+
border-bottom: 1px solid #1e293b;
|
|
10
|
+
padding-bottom: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&__title {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 24px;
|
|
16
|
+
color: #62c9ff;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__content {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 20px;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Chemical X Protocol: Foundational Atom Tier
|
|
4
|
+
* Directive: Atoms are the ONLY tier where raw DOM/HTML elements are permitted.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
type?: 'button' | 'submit' | 'reset';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
variant?: 'primary' | 'ghost' | 'glass';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
type: 'button',
|
|
15
|
+
disabled: false,
|
|
16
|
+
ariaLabel: undefined,
|
|
17
|
+
variant: 'primary'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: 'click', event: MouseEvent): void;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const handleClick = (event: MouseEvent) => {
|
|
25
|
+
if (props.disabled) return;
|
|
26
|
+
emit('click', event);
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<button
|
|
32
|
+
:type="props.type"
|
|
33
|
+
:disabled="props.disabled"
|
|
34
|
+
:aria-label="props.ariaLabel"
|
|
35
|
+
class="a-button cursor-pointer"
|
|
36
|
+
:data-variant="props.variant"
|
|
37
|
+
@click="handleClick"
|
|
38
|
+
>
|
|
39
|
+
<slot name="prepend" />
|
|
40
|
+
<slot />
|
|
41
|
+
<slot name="append" />
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.m-sample-card {
|
|
2
|
+
background: #131e3a;
|
|
3
|
+
border: 1px solid #1e293b;
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
padding: 16px;
|
|
6
|
+
|
|
7
|
+
&__header {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: flex-start;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&__title-group {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&__title {
|
|
19
|
+
margin: 0;
|
|
20
|
+
font-size: 16px;
|
|
21
|
+
color: #ffffff;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__subtitle {
|
|
25
|
+
margin: 4px 0 0;
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
color: #94a3b8;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&__badge {
|
|
31
|
+
font-size: 11px;
|
|
32
|
+
padding: 2px 8px;
|
|
33
|
+
border-radius: 4px;
|
|
34
|
+
background: #0b1329;
|
|
35
|
+
|
|
36
|
+
&--archived {
|
|
37
|
+
color: #f87171;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&--high-value {
|
|
41
|
+
color: #4ade80;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&--standard {
|
|
45
|
+
color: #86efac;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&__body {
|
|
50
|
+
margin-top: 16px;
|
|
51
|
+
display: flex;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
align-items: center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__value {
|
|
57
|
+
font-size: 20px;
|
|
58
|
+
font-weight: bold;
|
|
59
|
+
color: #62c9ff;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&__action {
|
|
63
|
+
padding: 6px 12px;
|
|
64
|
+
background: #1e293b;
|
|
65
|
+
border: 1px solid #334155;
|
|
66
|
+
color: #ffffff;
|
|
67
|
+
border-radius: 4px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
transition: background-color 0.15s ease-in-out;
|
|
70
|
+
|
|
71
|
+
&:hover {
|
|
72
|
+
background: #334155;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.m-chemx-badge {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.5rem;
|
|
5
|
+
padding: 0.25rem 0.75rem;
|
|
6
|
+
border-radius: 9999px;
|
|
7
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
8
|
+
font-size: 0.7rem;
|
|
9
|
+
text-decoration: none;
|
|
10
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
11
|
+
background: rgba(9, 13, 22, 0.85);
|
|
12
|
+
backdrop-filter: blur(8px);
|
|
13
|
+
-webkit-backdrop-filter: blur(8px);
|
|
14
|
+
color: #cbd5e1;
|
|
15
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
16
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
17
|
+
|
|
18
|
+
&:hover {
|
|
19
|
+
border-color: rgba(56, 189, 248, 0.5);
|
|
20
|
+
box-shadow: 0 0 12px rgba(56, 189, 248, 0.25);
|
|
21
|
+
transform: scale(1.02);
|
|
22
|
+
color: #f8fafc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&__dot {
|
|
26
|
+
width: 0.5rem;
|
|
27
|
+
height: 0.5rem;
|
|
28
|
+
border-radius: 50%;
|
|
29
|
+
background-color: #a3e635;
|
|
30
|
+
box-shadow: 0 0 8px rgba(163, 230, 53, 0.6);
|
|
31
|
+
animation: chemx-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__label {
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&__grade {
|
|
39
|
+
padding: 0.1rem 0.4rem;
|
|
40
|
+
border-radius: 9999px;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
font-size: 0.65rem;
|
|
43
|
+
letter-spacing: -0.02em;
|
|
44
|
+
|
|
45
|
+
&--a {
|
|
46
|
+
background: rgba(163, 230, 53, 0.15);
|
|
47
|
+
color: #a3e635;
|
|
48
|
+
border: 1px solid rgba(163, 230, 53, 0.3);
|
|
49
|
+
}
|
|
50
|
+
&--b {
|
|
51
|
+
background: rgba(56, 189, 248, 0.15);
|
|
52
|
+
color: #38bdf8;
|
|
53
|
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
|
54
|
+
}
|
|
55
|
+
&--c {
|
|
56
|
+
background: rgba(251, 191, 36, 0.15);
|
|
57
|
+
color: #fbbf24;
|
|
58
|
+
border: 1px solid rgba(251, 191, 36, 0.3);
|
|
59
|
+
}
|
|
60
|
+
&--d {
|
|
61
|
+
background: rgba(249, 115, 22, 0.15);
|
|
62
|
+
color: #f97316;
|
|
63
|
+
border: 1px solid rgba(249, 115, 22, 0.3);
|
|
64
|
+
}
|
|
65
|
+
&--f {
|
|
66
|
+
background: rgba(244, 63, 133, 0.15);
|
|
67
|
+
color: #f43f85;
|
|
68
|
+
border: 1px solid rgba(244, 63, 133, 0.3);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes chemx-pulse {
|
|
74
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
75
|
+
50% { opacity: 0.5; transform: scale(0.9); }
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { MChemxBadge, resolveGradeClass } from './m-chemx-badge';
|
|
5
|
+
|
|
6
|
+
describe('m-chemx-badge: resolveGradeClass', () => {
|
|
7
|
+
it('resolves tier-a grade class for grade A variants', () => {
|
|
8
|
+
expect(resolveGradeClass('A+')).toBe('m-chemx-badge__grade--a');
|
|
9
|
+
expect(resolveGradeClass('a')).toBe('m-chemx-badge__grade--a');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('resolves tier-b grade class for grade B variants', () => {
|
|
13
|
+
expect(resolveGradeClass('B+')).toBe('m-chemx-badge__grade--b');
|
|
14
|
+
expect(resolveGradeClass('b')).toBe('m-chemx-badge__grade--b');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('resolves tier-c grade class for grade C variants', () => {
|
|
18
|
+
expect(resolveGradeClass('C')).toBe('m-chemx-badge__grade--c');
|
|
19
|
+
expect(resolveGradeClass('c')).toBe('m-chemx-badge__grade--c');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('resolves tier-d grade class for grade D variants', () => {
|
|
23
|
+
expect(resolveGradeClass('D')).toBe('m-chemx-badge__grade--d');
|
|
24
|
+
expect(resolveGradeClass('d')).toBe('m-chemx-badge__grade--d');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resolves tier-f grade class for grade F and unclassified inputs', () => {
|
|
28
|
+
expect(resolveGradeClass('F')).toBe('m-chemx-badge__grade--f');
|
|
29
|
+
expect(resolveGradeClass('unknown')).toBe('m-chemx-badge__grade--f');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('m-chemx-badge: MChemxBadge Component', () => {
|
|
34
|
+
it('renders default badge markup with secure attributes', () => {
|
|
35
|
+
const html = renderToStaticMarkup(React.createElement(MChemxBadge));
|
|
36
|
+
|
|
37
|
+
expect(html).toContain('href="https://chemicalx.xophz.com"');
|
|
38
|
+
expect(html).toContain('target="_blank"');
|
|
39
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
40
|
+
expect(html).toContain('title="Verified by Chemical X Molecular Architecture Protocol"');
|
|
41
|
+
expect(html).toContain('class="m-chemx-badge"');
|
|
42
|
+
expect(html).toContain('class="m-chemx-badge__dot"');
|
|
43
|
+
expect(html).toContain('AI Slop cleaned with Chemical X');
|
|
44
|
+
expect(html).toContain('class="m-chemx-badge__grade m-chemx-badge__grade--a"');
|
|
45
|
+
expect(html).toContain('[A+]');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('prioritizes reportUrl over standard href when provided', () => {
|
|
49
|
+
const customReport = 'https://reports.chemicalx.xophz.com/audit/42';
|
|
50
|
+
const html = renderToStaticMarkup(
|
|
51
|
+
React.createElement(MChemxBadge, {
|
|
52
|
+
reportUrl: customReport,
|
|
53
|
+
href: 'https://fallback.com'
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(html).toContain(`href="${customReport}"`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies custom label, grade, and className correctly', () => {
|
|
61
|
+
const html = renderToStaticMarkup(
|
|
62
|
+
React.createElement(MChemxBadge, {
|
|
63
|
+
label: 'Custom Verified Audit',
|
|
64
|
+
grade: 'B',
|
|
65
|
+
className: 'custom-anchor-modifier'
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(html).toContain('class="m-chemx-badge custom-anchor-modifier"');
|
|
70
|
+
expect(html).toContain('Custom Verified Audit');
|
|
71
|
+
expect(html).toContain('class="m-chemx-badge__grade m-chemx-badge__grade--b"');
|
|
72
|
+
expect(html).toContain('[B]');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MChemxBadgeProps } from './types';
|
|
3
|
+
|
|
4
|
+
export const resolveGradeClass = (grade: string): string => {
|
|
5
|
+
const g = grade.toUpperCase();
|
|
6
|
+
if (g.startsWith('A')) return 'm-chemx-badge__grade--a';
|
|
7
|
+
if (g.startsWith('B')) return 'm-chemx-badge__grade--b';
|
|
8
|
+
if (g.startsWith('C')) return 'm-chemx-badge__grade--c';
|
|
9
|
+
if (g.startsWith('D')) return 'm-chemx-badge__grade--d';
|
|
10
|
+
return 'm-chemx-badge__grade--f';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const MChemxBadge: React.FC<MChemxBadgeProps> = ({
|
|
14
|
+
label = 'AI Slop cleaned with Chemical X',
|
|
15
|
+
grade = 'A+',
|
|
16
|
+
href = 'https://chemicalx.xophz.com',
|
|
17
|
+
reportUrl,
|
|
18
|
+
className = ''
|
|
19
|
+
}) => {
|
|
20
|
+
const gradeClass = resolveGradeClass(grade);
|
|
21
|
+
const targetHref = reportUrl || href;
|
|
22
|
+
const rootClass = `m-chemx-badge ${className}`.trim();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<a
|
|
26
|
+
href={targetHref}
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
className={rootClass}
|
|
30
|
+
title="Verified by Chemical X Molecular Architecture Protocol"
|
|
31
|
+
>
|
|
32
|
+
<span className="m-chemx-badge__dot" />
|
|
33
|
+
<span className="m-chemx-badge__label">{label}</span>
|
|
34
|
+
<span className={`m-chemx-badge__grade ${gradeClass}`}>[{grade}]</span>
|
|
35
|
+
</a>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default MChemxBadge;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { MChemxBadgeProps } from './types';
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<MChemxBadgeProps>(), {
|
|
6
|
+
label: 'AI Slop cleaned with Chemical X',
|
|
7
|
+
grade: 'A+',
|
|
8
|
+
href: 'https://chemicalx.xophz.com',
|
|
9
|
+
reportUrl: undefined,
|
|
10
|
+
className: ''
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// 1. Composables & Stores
|
|
14
|
+
|
|
15
|
+
// 2. Reactive Primitives
|
|
16
|
+
|
|
17
|
+
// 3. Computed State
|
|
18
|
+
const targetHref = computed(() => props.reportUrl || props.href);
|
|
19
|
+
const gradeClass = computed(() => {
|
|
20
|
+
const g = props.grade.toUpperCase();
|
|
21
|
+
if (g.startsWith('A')) return 'm-chemx-badge__grade--a';
|
|
22
|
+
if (g.startsWith('B')) return 'm-chemx-badge__grade--b';
|
|
23
|
+
if (g.startsWith('C')) return 'm-chemx-badge__grade--c';
|
|
24
|
+
if (g.startsWith('D')) return 'm-chemx-badge__grade--d';
|
|
25
|
+
return 'm-chemx-badge__grade--f';
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<a
|
|
31
|
+
:href="targetHref"
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
class="m-chemx-badge"
|
|
35
|
+
:class="className"
|
|
36
|
+
title="Verified by Chemical X Molecular Architecture Protocol"
|
|
37
|
+
>
|
|
38
|
+
<span class="m-chemx-badge__dot" />
|
|
39
|
+
<span class="m-chemx-badge__label">{{ label }}</span>
|
|
40
|
+
<span class="m-chemx-badge__grade" :class="gradeClass">[{{ grade }}]</span>
|
|
41
|
+
</a>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { MSampleCard, resolveBadgeDescriptor } from './m-sample-card';
|
|
5
|
+
|
|
6
|
+
describe('m-sample-card: resolveBadgeDescriptor', () => {
|
|
7
|
+
it('returns archived badge descriptor when status is archived', () => {
|
|
8
|
+
const descriptor = resolveBadgeDescriptor('archived', false);
|
|
9
|
+
expect(descriptor.text).toBe('archived');
|
|
10
|
+
expect(descriptor.className).toBe('m-sample-card__badge m-sample-card__badge--archived');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns high-value badge descriptor when active and value exceeds threshold', () => {
|
|
14
|
+
const descriptor = resolveBadgeDescriptor('active', true);
|
|
15
|
+
expect(descriptor.text).toBe('active');
|
|
16
|
+
expect(descriptor.className).toBe('m-sample-card__badge m-sample-card__badge--high-value');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns standard badge descriptor when active and value is within standard range', () => {
|
|
20
|
+
const descriptor = resolveBadgeDescriptor('active', false);
|
|
21
|
+
expect(descriptor.text).toBe('active');
|
|
22
|
+
expect(descriptor.className).toBe('m-sample-card__badge m-sample-card__badge--standard');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('m-sample-card: MSampleCard Component', () => {
|
|
27
|
+
it('renders title, formatted value, and standard badge correctly', () => {
|
|
28
|
+
const html = renderToStaticMarkup(
|
|
29
|
+
React.createElement(MSampleCard, {
|
|
30
|
+
title: 'Core Subscription',
|
|
31
|
+
subtitle: 'Monthly compute tier',
|
|
32
|
+
value: 750
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(html).toContain('class="m-sample-card"');
|
|
37
|
+
expect(html).toContain('Core Subscription');
|
|
38
|
+
expect(html).toContain('Monthly compute tier');
|
|
39
|
+
expect(html).toContain('$750');
|
|
40
|
+
expect(html).toContain('m-sample-card__badge--standard');
|
|
41
|
+
expect(html).not.toContain('m-sample-card__action');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders high-value modifier when numeric value exceeds 1000', () => {
|
|
45
|
+
const html = renderToStaticMarkup(
|
|
46
|
+
React.createElement(MSampleCard, {
|
|
47
|
+
title: 'Enterprise Cluster',
|
|
48
|
+
value: 2500
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(html).toContain('Enterprise Cluster');
|
|
53
|
+
expect(html).toContain('$2,500');
|
|
54
|
+
expect(html).toContain('m-sample-card__badge--high-value');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders action button and triggers onAction callback when clicked', () => {
|
|
58
|
+
const handleAction = vi.fn();
|
|
59
|
+
const vnode = MSampleCard({
|
|
60
|
+
title: 'Actionable Unit',
|
|
61
|
+
value: 1200,
|
|
62
|
+
onAction: handleAction
|
|
63
|
+
}) as React.ReactElement;
|
|
64
|
+
|
|
65
|
+
const bodyNode = (vnode.props.children as React.ReactElement[])[1];
|
|
66
|
+
const buttonNode = (bodyNode.props.children as React.ReactElement[])[1];
|
|
67
|
+
|
|
68
|
+
expect(buttonNode).toBeDefined();
|
|
69
|
+
expect(buttonNode.props.className).toBe('m-sample-card__action');
|
|
70
|
+
|
|
71
|
+
buttonNode.props.onClick();
|
|
72
|
+
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MSampleCardProps, SampleCardBadgeDescriptor } from './types';
|
|
3
|
+
|
|
4
|
+
export const resolveBadgeDescriptor = (
|
|
5
|
+
status: 'active' | 'archived',
|
|
6
|
+
isHighValue: boolean
|
|
7
|
+
): SampleCardBadgeDescriptor => {
|
|
8
|
+
if (status !== 'active') {
|
|
9
|
+
return {
|
|
10
|
+
text: status,
|
|
11
|
+
className: 'm-sample-card__badge m-sample-card__badge--archived'
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isHighValue) {
|
|
16
|
+
return {
|
|
17
|
+
text: status,
|
|
18
|
+
className: 'm-sample-card__badge m-sample-card__badge--high-value'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
text: status,
|
|
24
|
+
className: 'm-sample-card__badge m-sample-card__badge--standard'
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const MSampleCard: React.FC<MSampleCardProps> = ({
|
|
29
|
+
title,
|
|
30
|
+
subtitle,
|
|
31
|
+
value,
|
|
32
|
+
status = 'active',
|
|
33
|
+
onAction
|
|
34
|
+
}) => {
|
|
35
|
+
const isHighValue = value > 1000;
|
|
36
|
+
const badge = resolveBadgeDescriptor(status, isHighValue);
|
|
37
|
+
|
|
38
|
+
const handleActionClick = () => {
|
|
39
|
+
if (!onAction) return;
|
|
40
|
+
onAction();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="m-sample-card">
|
|
45
|
+
<div className="m-sample-card__header">
|
|
46
|
+
<div className="m-sample-card__title-group">
|
|
47
|
+
<h3 className="m-sample-card__title">{title}</h3>
|
|
48
|
+
{subtitle && <p className="m-sample-card__subtitle">{subtitle}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
<span className={badge.className}>
|
|
51
|
+
{badge.text}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="m-sample-card__body">
|
|
55
|
+
<div className="m-sample-card__value">
|
|
56
|
+
${value.toLocaleString()}
|
|
57
|
+
</div>
|
|
58
|
+
{onAction && (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="m-sample-card__action"
|
|
62
|
+
onClick={handleActionClick}
|
|
63
|
+
>
|
|
64
|
+
Action
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default MSampleCard;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.m-tab-button {
|
|
2
|
+
padding: 0.375rem 0.625rem;
|
|
3
|
+
border-radius: 0.5rem;
|
|
4
|
+
font-size: 0.75rem;
|
|
5
|
+
font-family: monospace;
|
|
6
|
+
font-weight: 700;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
gap: 0.375rem;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
transition: all 0.2s ease-in-out;
|
|
13
|
+
|
|
14
|
+
&--flexible {
|
|
15
|
+
flex: 1 1 0%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&--inactive {
|
|
19
|
+
color: var(--color-slate-400, #94a3b8);
|
|
20
|
+
|
|
21
|
+
&:hover {
|
|
22
|
+
color: var(--color-slate-200, #e2e8f0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&--lime {
|
|
27
|
+
background-color: #84cc16;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
box-shadow: 0 4px 6px -1px rgba(132, 204, 22, 0.2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&--pink {
|
|
33
|
+
background-color: #ec4899;
|
|
34
|
+
color: #ffffff;
|
|
35
|
+
box-shadow: 0 4px 6px -1px rgba(236, 72, 153, 0.2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&--sky {
|
|
39
|
+
background-color: #0ea5e9;
|
|
40
|
+
color: #ffffff;
|
|
41
|
+
box-shadow: 0 4px 6px -1px rgba(14, 165, 233, 0.2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&--slate {
|
|
45
|
+
background-color: #334155;
|
|
46
|
+
color: #ffffff;
|
|
47
|
+
box-shadow: 0 4px 6px -1px rgba(51, 65, 85, 0.2);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import type { TabButtonProps, TabTone } from './types';
|
|
3
|
+
|
|
4
|
+
export function useTabButtonController(props: TabButtonProps, emit: (event: 'click' | 'select') => void) {
|
|
5
|
+
const isSelected = computed((): boolean => Boolean(props.isActive));
|
|
6
|
+
|
|
7
|
+
const resolveToneClass = (tone: TabTone = 'sky'): string => {
|
|
8
|
+
switch (tone) {
|
|
9
|
+
case 'lime':
|
|
10
|
+
return 'm-tab-button--lime';
|
|
11
|
+
case 'pink':
|
|
12
|
+
return 'm-tab-button--pink';
|
|
13
|
+
case 'slate':
|
|
14
|
+
return 'm-tab-button--slate';
|
|
15
|
+
case 'sky':
|
|
16
|
+
default:
|
|
17
|
+
return 'm-tab-button--sky';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const stateClass = computed((): string => {
|
|
22
|
+
return isSelected.value ? resolveToneClass(props.activeTone) : 'm-tab-button--inactive';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const flexClass = computed((): string => {
|
|
26
|
+
return props.isFlexible ? 'm-tab-button--flexible' : '';
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const modifierClasses = computed((): string[] => {
|
|
30
|
+
return [stateClass.value, flexClass.value].filter(Boolean);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const handleClick = () => {
|
|
34
|
+
emit('click');
|
|
35
|
+
emit('select');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isSelected,
|
|
40
|
+
modifierClasses,
|
|
41
|
+
handleClick
|
|
42
|
+
};
|
|
43
|
+
}
|