@dezkareid/components 0.0.0 → 1.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.
Files changed (87) hide show
  1. package/README.md +213 -5
  2. package/dist/_virtual/_commonjsHelpers.js +6 -0
  3. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  4. package/dist/_virtual/index.js +8 -0
  5. package/dist/_virtual/index.js.map +1 -0
  6. package/dist/_virtual/index2.js +4 -0
  7. package/dist/_virtual/index2.js.map +1 -0
  8. package/dist/astro/index.d.ts +5 -0
  9. package/dist/astro/index.d.ts.map +1 -0
  10. package/dist/components.min.css +1 -0
  11. package/dist/css/button.module.css.js +4 -0
  12. package/dist/css/button.module.css.js.map +1 -0
  13. package/dist/css/card.module.css.js +4 -0
  14. package/dist/css/card.module.css.js.map +1 -0
  15. package/dist/css/index.d.ts +5 -0
  16. package/dist/css/index.d.ts.map +1 -0
  17. package/dist/css/tag.module.css.js +4 -0
  18. package/dist/css/tag.module.css.js.map +1 -0
  19. package/dist/css/theme-toggle.module.css.js +4 -0
  20. package/dist/css/theme-toggle.module.css.js.map +1 -0
  21. package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js +86 -0
  22. package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js.map +1 -0
  23. package/dist/react/Button/index.d.ts +6 -0
  24. package/dist/react/Button/index.d.ts.map +1 -0
  25. package/dist/react/Button/index.js +10 -0
  26. package/dist/react/Button/index.js.map +1 -0
  27. package/dist/react/Card/index.d.ts +6 -0
  28. package/dist/react/Card/index.d.ts.map +1 -0
  29. package/dist/react/Card/index.js +10 -0
  30. package/dist/react/Card/index.js.map +1 -0
  31. package/dist/react/Tag/index.d.ts +6 -0
  32. package/dist/react/Tag/index.d.ts.map +1 -0
  33. package/dist/react/Tag/index.js +10 -0
  34. package/dist/react/Tag/index.js.map +1 -0
  35. package/dist/react/ThemeToggle/index.d.ts +2 -0
  36. package/dist/react/ThemeToggle/index.d.ts.map +1 -0
  37. package/dist/react/ThemeToggle/index.js +25 -0
  38. package/dist/react/ThemeToggle/index.js.map +1 -0
  39. package/dist/react/index.d.ts +5 -0
  40. package/dist/react/index.d.ts.map +1 -0
  41. package/dist/react/index.js +5 -0
  42. package/dist/react/index.js.map +1 -0
  43. package/dist/shared/js/theme.d.ts +5 -0
  44. package/dist/shared/js/theme.d.ts.map +1 -0
  45. package/dist/shared/js/theme.js +22 -0
  46. package/dist/shared/js/theme.js.map +1 -0
  47. package/dist/shared/types/button.d.ts +8 -0
  48. package/dist/shared/types/button.d.ts.map +1 -0
  49. package/dist/shared/types/card.d.ts +5 -0
  50. package/dist/shared/types/card.d.ts.map +1 -0
  51. package/dist/shared/types/tag.d.ts +5 -0
  52. package/dist/shared/types/tag.d.ts.map +1 -0
  53. package/dist/shared/types/theme-toggle.d.ts +4 -0
  54. package/dist/shared/types/theme-toggle.d.ts.map +1 -0
  55. package/dist/vue/index.d.ts +5 -0
  56. package/dist/vue/index.d.ts.map +1 -0
  57. package/package.json +81 -6
  58. package/src/astro/Button/index.astro +35 -0
  59. package/src/astro/Card/index.astro +23 -0
  60. package/src/astro/Tag/index.astro +23 -0
  61. package/src/astro/ThemeToggle/index.astro +63 -0
  62. package/src/astro/index.ts +4 -0
  63. package/src/css/button.module.css +90 -0
  64. package/src/css/card.module.css +30 -0
  65. package/src/css/index.ts +4 -0
  66. package/src/css/tag.module.css +33 -0
  67. package/src/css/theme-toggle.module.css +38 -0
  68. package/src/declarations.d.ts +19 -0
  69. package/src/react/Button/index.test.tsx +59 -0
  70. package/src/react/Button/index.tsx +31 -0
  71. package/src/react/Card/index.test.tsx +38 -0
  72. package/src/react/Card/index.tsx +14 -0
  73. package/src/react/Tag/index.test.tsx +35 -0
  74. package/src/react/Tag/index.tsx +14 -0
  75. package/src/react/ThemeToggle/index.test.tsx +84 -0
  76. package/src/react/ThemeToggle/index.tsx +36 -0
  77. package/src/react/index.ts +4 -0
  78. package/src/shared/js/theme.ts +22 -0
  79. package/src/shared/types/button.ts +8 -0
  80. package/src/shared/types/card.ts +5 -0
  81. package/src/shared/types/tag.ts +5 -0
  82. package/src/shared/types/theme-toggle.ts +5 -0
  83. package/src/vue/Button/index.vue +27 -0
  84. package/src/vue/Card/index.vue +18 -0
  85. package/src/vue/Tag/index.vue +18 -0
  86. package/src/vue/ThemeToggle/index.vue +39 -0
  87. package/src/vue/index.ts +4 -0
@@ -0,0 +1,5 @@
1
+ export type TagVariant = 'default' | 'success' | 'danger';
2
+ export interface TagProps {
3
+ variant?: TagVariant;
4
+ }
5
+ //# sourceMappingURL=tag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/shared/types/tag.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB"}
@@ -0,0 +1,4 @@
1
+ export type Theme = 'light' | 'dark';
2
+ export interface ThemeToggleProps {
3
+ }
4
+ //# sourceMappingURL=theme-toggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-toggle.d.ts","sourceRoot":"","sources":["../../../src/shared/types/theme-toggle.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AAErC,MAAM,WAAW,gBAAgB;CAEhC"}
@@ -0,0 +1,5 @@
1
+ export { default as Button } from './Button/index.vue';
2
+ export { default as Tag } from './Tag/index.vue';
3
+ export { default as Card } from './Card/index.vue';
4
+ export { default as ThemeToggle } from './ThemeToggle/index.vue';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,33 @@
1
1
  {
2
2
  "name": "@dezkareid/components",
3
- "version": "0.0.0",
3
+ "version": "1.0.1",
4
+ "type": "module",
4
5
  "description": "A package to export UI components in formats like React, Astro, Vue, etc.",
6
+ "files": [
7
+ "dist",
8
+ "src",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ "./react": {
13
+ "types": "./dist/react/index.d.ts",
14
+ "import": "./dist/react.js",
15
+ "default": "./dist/react.js"
16
+ },
17
+ "./astro": "./src/astro/index.ts",
18
+ "./vue": "./src/vue/index.ts",
19
+ "./css": {
20
+ "import": "./dist/components.min.css",
21
+ "default": "./dist/components.min.css"
22
+ }
23
+ },
24
+ "typesVersions": {
25
+ "*": {
26
+ "react": [
27
+ "./dist/react/index.d.ts"
28
+ ]
29
+ }
30
+ },
5
31
  "repository": {
6
32
  "type": "git",
7
33
  "url": "https://github.com/dezkareid/dezkareid"
@@ -15,8 +41,57 @@
15
41
  "access": "public"
16
42
  },
17
43
  "keywords": [
18
- "setup",
19
- "package",
20
- "npm"
21
- ]
22
- }
44
+ "design-system",
45
+ "components",
46
+ "react",
47
+ "astro",
48
+ "vue"
49
+ ],
50
+ "peerDependencies": {
51
+ "astro": ">=4.0.0",
52
+ "react": "^18.0.0 || ^19.0.0",
53
+ "react-dom": "^18.0.0 || ^19.0.0",
54
+ "vue": "^3.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "astro": {
58
+ "optional": true
59
+ },
60
+ "vue": {
61
+ "optional": true
62
+ },
63
+ "react": {
64
+ "optional": true
65
+ },
66
+ "react-dom": {
67
+ "optional": true
68
+ }
69
+ },
70
+ "dependencies": {
71
+ "classnames": "2.5.1",
72
+ "@dezkareid/design-tokens": "0.0.0"
73
+ },
74
+ "devDependencies": {
75
+ "@rollup/plugin-commonjs": "29.0.0",
76
+ "@rollup/plugin-node-resolve": "16.0.3",
77
+ "@rollup/plugin-typescript": "12.3.0",
78
+ "@testing-library/jest-dom": "6.9.1",
79
+ "@testing-library/react": "16.3.2",
80
+ "@testing-library/user-event": "14.5.2",
81
+ "@types/classnames": "^2.3.4",
82
+ "@types/react": "19.2.9",
83
+ "@vitejs/plugin-react": "5.1.4",
84
+ "jsdom": "27.4.0",
85
+ "react": "19.2.4",
86
+ "react-dom": "19.2.4",
87
+ "rollup": "4.56.0",
88
+ "rollup-plugin-postcss": "4.0.2",
89
+ "typescript": "5.9.3",
90
+ "vite": "7.3.1",
91
+ "vitest": "4.0.18"
92
+ },
93
+ "scripts": {
94
+ "build": "rollup -c rollup.config.mjs",
95
+ "test": "vitest --run"
96
+ }
97
+ }
@@ -0,0 +1,35 @@
1
+ ---
2
+ import type { ButtonProps } from '../../shared/types/button';
3
+ import styles from '../../css/button.module.css';
4
+
5
+ type Props = ButtonProps & {
6
+ href?: string;
7
+ class?: string;
8
+ [key: string]: unknown;
9
+ };
10
+
11
+ const {
12
+ variant = 'primary',
13
+ size = 'md',
14
+ disabled = false,
15
+ href,
16
+ class: className,
17
+ ...rest
18
+ } = Astro.props;
19
+
20
+ const Tag = href ? 'a' : 'button';
21
+
22
+ const classes = [
23
+ styles.button,
24
+ styles[`button--${variant}`],
25
+ styles[`button--${size}`],
26
+ disabled ? styles['button--disabled'] : '',
27
+ className ?? '',
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ');
31
+ ---
32
+
33
+ <Tag class={classes} href={href} disabled={!href && disabled} {...rest}>
34
+ <slot />
35
+ </Tag>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import type { CardProps } from '../../shared/types/card';
3
+ import styles from '../../css/card.module.css';
4
+
5
+ type Props = CardProps & {
6
+ class?: string;
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ const { elevation = 'raised', class: className, ...rest } = Astro.props;
11
+
12
+ const classes = [
13
+ styles.card,
14
+ styles[`card--${elevation}`],
15
+ className ?? '',
16
+ ]
17
+ .filter(Boolean)
18
+ .join(' ');
19
+ ---
20
+
21
+ <div class={classes} {...rest}>
22
+ <slot />
23
+ </div>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import type { TagProps } from '../../shared/types/tag';
3
+ import styles from '../../css/tag.module.css';
4
+
5
+ type Props = TagProps & {
6
+ class?: string;
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ const { variant = 'default', class: className, ...rest } = Astro.props;
11
+
12
+ const classes = [
13
+ styles.tag,
14
+ styles[`tag--${variant}`],
15
+ className ?? '',
16
+ ]
17
+ .filter(Boolean)
18
+ .join(' ');
19
+ ---
20
+
21
+ <span class={classes} {...rest}>
22
+ <slot />
23
+ </span>
@@ -0,0 +1,63 @@
1
+ ---
2
+ import styles from '../../css/theme-toggle.module.css';
3
+
4
+ type Props = {
5
+ class?: string;
6
+ };
7
+
8
+ const { class: className } = Astro.props;
9
+
10
+ const classes = [styles['theme-toggle'], className ?? ''].filter(Boolean).join(' ');
11
+ ---
12
+
13
+ <!--
14
+ The inline script runs before first paint to apply the stored theme,
15
+ preventing a flash of unstyled/wrong-theme content (FOUC).
16
+ -->
17
+ <script is:inline>
18
+ (function () {
19
+ var stored = localStorage.getItem('color-scheme');
20
+ var theme =
21
+ stored === 'light' || stored === 'dark'
22
+ ? stored
23
+ : window.matchMedia('(prefers-color-scheme: dark)').matches
24
+ ? 'dark'
25
+ : 'light';
26
+ document.documentElement.setAttribute('color-scheme', theme);
27
+ })();
28
+ </script>
29
+
30
+ <button
31
+ type="button"
32
+ class={classes}
33
+ data-theme-toggle
34
+ aria-label="Toggle colour scheme"
35
+ >
36
+ <span data-theme-label>Light</span>
37
+ </button>
38
+
39
+ <script>
40
+ import { getInitialTheme, applyTheme, persistTheme } from '../../shared/js/theme';
41
+
42
+ const btn = document.querySelector('[data-theme-toggle]') as HTMLButtonElement | null;
43
+ const label = document.querySelector('[data-theme-label]') as HTMLElement | null;
44
+
45
+ if (btn && label) {
46
+ let current = getInitialTheme();
47
+
48
+ function update(theme: 'light' | 'dark') {
49
+ label!.textContent = theme === 'dark' ? 'Dark' : 'Light';
50
+ btn!.setAttribute('aria-pressed', String(theme === 'dark'));
51
+ btn!.classList.toggle('theme-toggle--dark', theme === 'dark');
52
+ }
53
+
54
+ update(current);
55
+
56
+ btn.addEventListener('click', () => {
57
+ current = current === 'light' ? 'dark' : 'light';
58
+ applyTheme(current);
59
+ persistTheme(current);
60
+ update(current);
61
+ });
62
+ }
63
+ </script>
@@ -0,0 +1,4 @@
1
+ export { default as Button } from './Button/index.astro';
2
+ export { default as Tag } from './Tag/index.astro';
3
+ export { default as Card } from './Card/index.astro';
4
+ export { default as ThemeToggle } from './ThemeToggle/index.astro';
@@ -0,0 +1,90 @@
1
+ /* =============================================================================
2
+ BUTTON — BEM + OOCSS
3
+ Structure classes: layout, sizing, spacing
4
+ Skin classes: colour, border, typography
5
+ ============================================================================= */
6
+
7
+ /* --- Structure: block --- */
8
+ .button {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ cursor: pointer;
13
+ border: none;
14
+ border-radius: var(--spacing-4);
15
+ font-family: var(--font-family-base);
16
+ font-weight: var(--font-weight-medium);
17
+ line-height: var(--font-line-height-none);
18
+ text-decoration: none;
19
+ white-space: nowrap;
20
+ transition: opacity 0.15s ease;
21
+ }
22
+
23
+ /* --- Structure: size modifiers --- */
24
+ .button--sm,
25
+ .button--small {
26
+ padding: var(--spacing-4) var(--spacing-12);
27
+ font-size: var(--font-size-200);
28
+ }
29
+
30
+ .button--md,
31
+ .button--medium {
32
+ padding: var(--spacing-8) var(--spacing-24);
33
+ font-size: var(--font-size-300);
34
+ }
35
+
36
+ .button--lg,
37
+ .button--large {
38
+ padding: var(--spacing-12) var(--spacing-32);
39
+ font-size: var(--font-size-400);
40
+ }
41
+
42
+ /* --- Skin: variant modifiers --- */
43
+ .button--primary {
44
+ background-color: var(--color-primary);
45
+ color: var(--color-text-inverse);
46
+ border: 1px solid transparent;
47
+ }
48
+
49
+ .button--primary:hover:not(.button--disabled) {
50
+ opacity: 0.88;
51
+ }
52
+
53
+ .button--secondary,
54
+ .button--outline {
55
+ background-color: transparent;
56
+ color: var(--color-primary);
57
+ border: 1px solid var(--color-primary);
58
+ }
59
+
60
+ .button--secondary:hover:not(.button--disabled),
61
+ .button--outline:hover:not(.button--disabled) {
62
+ background-color: var(--color-background-secondary);
63
+ }
64
+
65
+ .button--success {
66
+ background-color: var(--color-success);
67
+ color: var(--color-text-inverse);
68
+ border: 1px solid transparent;
69
+ }
70
+
71
+ .button--success:hover:not(.button--disabled) {
72
+ opacity: 0.88;
73
+ }
74
+
75
+ .button--ghost {
76
+ background-color: transparent;
77
+ color: var(--color-text-primary);
78
+ border: 1px solid transparent;
79
+ }
80
+
81
+ .button--ghost:hover:not(.button--disabled) {
82
+ background-color: var(--color-background-secondary);
83
+ }
84
+
85
+ /* --- Skin: state modifier --- */
86
+ .button--disabled {
87
+ opacity: 0.4;
88
+ cursor: not-allowed;
89
+ pointer-events: none;
90
+ }
@@ -0,0 +1,30 @@
1
+ /* =============================================================================
2
+ CARD — BEM + OOCSS
3
+ Structure classes: layout, sizing, spacing
4
+ Skin classes: colour, shadow
5
+ ============================================================================= */
6
+
7
+ /* --- Structure: block --- */
8
+ .card {
9
+ display: block;
10
+ width: 100%;
11
+ border-radius: var(--spacing-8);
12
+ padding: var(--spacing-24);
13
+ }
14
+
15
+ /* --- Skin: base --- */
16
+ .card {
17
+ background-color: var(--color-background-secondary);
18
+ color: var(--color-text-primary);
19
+ }
20
+
21
+ /* --- Skin: elevation modifiers --- */
22
+
23
+ /* TODO: Propose --shadow-raised token to the design-tokens package. */
24
+ .card--raised {
25
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
26
+ }
27
+
28
+ .card--flat {
29
+ box-shadow: none;
30
+ }
@@ -0,0 +1,4 @@
1
+ import './button.module.css';
2
+ import './card.module.css';
3
+ import './tag.module.css';
4
+ import './theme-toggle.module.css';
@@ -0,0 +1,33 @@
1
+ /* =============================================================================
2
+ TAG — BEM + OOCSS
3
+ Structure classes: layout, sizing, spacing
4
+ Skin classes: colour, border, typography
5
+ ============================================================================= */
6
+
7
+ /* --- Structure: block --- */
8
+ .tag {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ border-radius: var(--spacing-4);
12
+ padding: var(--spacing-4) var(--spacing-8);
13
+ font-family: var(--font-family-base);
14
+ font-size: var(--font-size-100);
15
+ font-weight: var(--font-weight-medium);
16
+ line-height: var(--font-line-height-none);
17
+ }
18
+
19
+ /* --- Skin: variant modifiers --- */
20
+ .tag--default {
21
+ background-color: var(--color-background-secondary);
22
+ color: var(--color-text-primary);
23
+ }
24
+
25
+ .tag--success {
26
+ background-color: var(--color-success);
27
+ color: var(--color-text-inverse);
28
+ }
29
+
30
+ .tag--danger {
31
+ background-color: var(--color-danger);
32
+ color: var(--color-text-inverse);
33
+ }
@@ -0,0 +1,38 @@
1
+ /* =============================================================================
2
+ THEME TOGGLE — BEM + OOCSS
3
+ Structure classes: layout, sizing, spacing
4
+ Skin classes: colour, state
5
+ ============================================================================= */
6
+
7
+ /* --- Structure: block --- */
8
+ .theme-toggle {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ gap: var(--spacing-8);
13
+ cursor: pointer;
14
+ padding: var(--spacing-8) var(--spacing-12);
15
+ border: none;
16
+ border-radius: var(--spacing-4);
17
+ font-family: var(--font-family-base);
18
+ font-size: var(--font-size-200);
19
+ font-weight: var(--font-weight-medium);
20
+ line-height: var(--font-line-height-none);
21
+ }
22
+
23
+ /* --- Skin: base --- */
24
+ .theme-toggle {
25
+ background-color: transparent;
26
+ color: var(--color-text-primary);
27
+ border: 1px solid var(--color-background-secondary);
28
+ }
29
+
30
+ .theme-toggle:hover {
31
+ background-color: var(--color-background-secondary);
32
+ }
33
+
34
+ /* --- Skin: state modifier (dark mode active) --- */
35
+ .theme-toggle--dark {
36
+ color: var(--color-primary);
37
+ border-color: var(--color-primary);
38
+ }
@@ -0,0 +1,19 @@
1
+ // CSS Modules
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>;
4
+ export default classes;
5
+ }
6
+
7
+ // Astro components
8
+ declare module '*.astro' {
9
+ import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
10
+ const Component: AstroComponentFactory;
11
+ export default Component;
12
+ }
13
+
14
+ // Vue SFCs
15
+ declare module '*.vue' {
16
+ import type { DefineComponent } from 'vue';
17
+ const Component: DefineComponent;
18
+ export default Component;
19
+ }
@@ -0,0 +1,59 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { Button } from './index';
5
+
6
+ describe('Button', () => {
7
+ it('renders with default variant and size', () => {
8
+ render(<Button>Click me</Button>);
9
+ expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders primary variant', () => {
13
+ render(<Button variant="primary">Primary</Button>);
14
+ const btn = screen.getByRole('button');
15
+ expect(btn.className).toMatch(/button--primary/);
16
+ });
17
+
18
+ it('renders secondary variant', () => {
19
+ render(<Button variant="secondary">Secondary</Button>);
20
+ const btn = screen.getByRole('button');
21
+ expect(btn.className).toMatch(/button--secondary/);
22
+ });
23
+
24
+ it('renders sm size', () => {
25
+ render(<Button size="sm">Small</Button>);
26
+ expect(screen.getByRole('button').className).toMatch(/button--sm/);
27
+ });
28
+
29
+ it('renders md size', () => {
30
+ render(<Button size="md">Medium</Button>);
31
+ expect(screen.getByRole('button').className).toMatch(/button--md/);
32
+ });
33
+
34
+ it('renders lg size', () => {
35
+ render(<Button size="lg">Large</Button>);
36
+ expect(screen.getByRole('button').className).toMatch(/button--lg/);
37
+ });
38
+
39
+ it('is disabled when disabled prop is set', () => {
40
+ render(<Button disabled>Disabled</Button>);
41
+ const btn = screen.getByRole('button');
42
+ expect(btn).toBeDisabled();
43
+ expect(btn.className).toMatch(/button--disabled/);
44
+ });
45
+
46
+ it('does not fire onClick when disabled', async () => {
47
+ const onClick = vi.fn();
48
+ render(<Button disabled onClick={onClick}>Disabled</Button>);
49
+ await userEvent.click(screen.getByRole('button'));
50
+ expect(onClick).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('fires onClick when enabled', async () => {
54
+ const onClick = vi.fn();
55
+ render(<Button onClick={onClick}>Click</Button>);
56
+ await userEvent.click(screen.getByRole('button'));
57
+ expect(onClick).toHaveBeenCalledOnce();
58
+ });
59
+ });
@@ -0,0 +1,31 @@
1
+ import type { ButtonHTMLAttributes } from 'react';
2
+ import cx from 'classnames';
3
+ import type { ButtonProps } from '../../shared/types/button';
4
+ import styles from '../../css/button.module.css';
5
+
6
+ type Props = ButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
7
+
8
+ export function Button({
9
+ variant = 'primary',
10
+ size = 'md',
11
+ disabled = false,
12
+ children,
13
+ className,
14
+ ...rest
15
+ }: Props) {
16
+ return (
17
+ <button
18
+ className={cx(
19
+ styles.button,
20
+ styles[`button--${variant}`],
21
+ styles[`button--${size}`],
22
+ disabled && styles['button--disabled'],
23
+ className,
24
+ )}
25
+ disabled={disabled}
26
+ {...rest}
27
+ >
28
+ {children}
29
+ </button>
30
+ );
31
+ }
@@ -0,0 +1,38 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { Card } from './index';
4
+
5
+ describe('Card', () => {
6
+ it('renders children', () => {
7
+ render(<Card>Card content</Card>);
8
+ expect(screen.getByText('Card content')).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders arbitrary children', () => {
12
+ render(
13
+ <Card>
14
+ <h2>Title</h2>
15
+ <p>Body</p>
16
+ </Card>
17
+ );
18
+ expect(screen.getByText('Title')).toBeInTheDocument();
19
+ expect(screen.getByText('Body')).toBeInTheDocument();
20
+ });
21
+
22
+ it('applies raised elevation by default', () => {
23
+ const { container } = render(<Card>Content</Card>);
24
+ expect((container.firstChild as HTMLElement).className).toMatch(/card--raised/);
25
+ });
26
+
27
+ it('applies flat elevation when set', () => {
28
+ const { container } = render(<Card elevation="flat">Content</Card>);
29
+ const el = container.firstChild as HTMLElement;
30
+ expect(el.className).toMatch(/card--flat/);
31
+ expect(el.className).not.toMatch(/card--raised/);
32
+ });
33
+
34
+ it('applies raised elevation when explicitly set', () => {
35
+ const { container } = render(<Card elevation="raised">Content</Card>);
36
+ expect((container.firstChild as HTMLElement).className).toMatch(/card--raised/);
37
+ });
38
+ });
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import cx from 'classnames';
3
+ import type { CardProps } from '../../shared/types/card';
4
+ import styles from '../../css/card.module.css';
5
+
6
+ type Props = CardProps & HTMLAttributes<HTMLDivElement>;
7
+
8
+ export function Card({ elevation = 'raised', children, className, ...rest }: Props) {
9
+ return (
10
+ <div className={cx(styles.card, styles[`card--${elevation}`], className)} {...rest}>
11
+ {children}
12
+ </div>
13
+ );
14
+ }