@chemx/create-chemx 26.9.16-668 → 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.
Files changed (62) hide show
  1. package/README.md +4 -0
  2. package/assets/chemx-starter-kit-logo.gif +0 -0
  3. package/blueprints/_view-template.scss +24 -0
  4. package/blueprints/atoms/a-button.vue +43 -0
  5. package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
  6. package/blueprints/molecule-capsule/index.ts +2 -0
  7. package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
  8. package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
  9. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
  10. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
  11. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
  12. package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
  13. package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
  14. package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
  15. package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
  16. package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
  17. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -0
  18. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
  19. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
  20. package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
  21. package/blueprints/molecule-capsule/types.d.ts +12 -0
  22. package/blueprints/shims-vue.d.ts +4 -0
  23. package/blueprints/view-template.tsx +20 -0
  24. package/blueprints/workflows/chemx-audit.yml +79 -0
  25. package/cli/audit/autofix.js +4 -5
  26. package/cli/audit/pattern-detector.js +90 -3
  27. package/cli/audit/pattern-detector.spec.js +36 -1
  28. package/cli/audit/rules-predicates.js +3 -2
  29. package/cli/audit-preflight.spec.js +11 -0
  30. package/cli/generator-templates/controller.js +36 -0
  31. package/cli/generator-templates/hooks.js +60 -0
  32. package/cli/generator-templates/naming.js +14 -0
  33. package/cli/generator-templates/react.js +63 -0
  34. package/cli/generator-templates/specs.js +38 -0
  35. package/cli/generator-templates/styles.js +64 -0
  36. package/cli/generator-templates/svelte.js +66 -0
  37. package/cli/generator-templates/types.js +29 -0
  38. package/cli/generator-templates/views.js +76 -0
  39. package/cli/generator-templates/vue.js +62 -0
  40. package/cli/generator-templates.d.ts +35 -0
  41. package/cli/generator-templates.js +28 -480
  42. package/cli/generator-templates.spec.js +108 -0
  43. package/cli/generator.js +2 -2
  44. package/cli/generator.spec.js +61 -1
  45. package/cli/index.js +3 -0
  46. package/cli/installer-templates.js +29 -8
  47. package/cli/license.js +22 -1
  48. package/cli/mcp/installer.js +31 -21
  49. package/cli/mcp/resources.js +114 -7
  50. package/cli/mcp/server.spec.js +34 -0
  51. package/cli/mcp/tools.js +14 -4
  52. package/cli/postinstall.js +27 -0
  53. package/cli/project-detector.js +20 -2
  54. package/cli/project-detector.spec.js +19 -0
  55. package/cli/reader.js +18 -4
  56. package/cli/scaffold.js +19 -3
  57. package/cli/terminal.js +36 -1
  58. package/docs/CHANGELOG.md +10 -0
  59. package/package.json +8 -5
  60. package/scripts/postinstall.mjs +27 -0
  61. package/scripts/pre-commit.sh +135 -0
  62. 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
  [![Live App](https://img.shields.io/badge/Web%20App-chemicalx.xophz.com-06b6d4?style=for-the-badge&logo=cloudflare)](https://chemicalx.xophz.com)
4
8
  [![Parent Repo](https://img.shields.io/badge/Repository-awesome--secret--sauce-8b5cf6?style=for-the-badge)](https://github.com/Chemical-X-Protocol/awesome-secret-sauce)
5
9
  [![Benchmarks](https://img.shields.io/badge/Benchmarks-Agent%20Evaluations-10b981?style=for-the-badge)](https://github.com/Chemical-X-Protocol/benchmarks)
@@ -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,2 @@
1
+ export { MSampleCard, resolveBadgeDescriptor } from './m-sample-card';
2
+ export type { MSampleCardProps, SampleCardBadgeDescriptor } from './types';
@@ -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,3 @@
1
+ export * from './types.d';
2
+ export { MChemxBadge, resolveGradeClass } from './m-chemx-badge';
3
+ export { default } from './m-chemx-badge';
@@ -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,12 @@
1
+ export interface MChemxBadgeProps {
2
+ label?: string;
3
+ grade?: string;
4
+ href?: string;
5
+ reportUrl?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export interface BadgeDescriptor {
10
+ colorClass: string;
11
+ dotClass: string;
12
+ }
@@ -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,3 @@
1
+ export { default as MTabButton } from './m-tab-button.vue';
2
+ export * from './m-tab-button.controller';
3
+ export * from './types';
@@ -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
+ }