@dezkareid/components 0.0.0 → 1.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.
Files changed (100) hide show
  1. package/.releaserc +18 -0
  2. package/.turbo/turbo-build.log +7 -0
  3. package/.turbo/turbo-test.log +17 -0
  4. package/AGENTS.md +174 -0
  5. package/CHANGELOG.md +12 -0
  6. package/README.md +213 -5
  7. package/dist/_virtual/_commonjsHelpers.js +6 -0
  8. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  9. package/dist/_virtual/index.js +8 -0
  10. package/dist/_virtual/index.js.map +1 -0
  11. package/dist/_virtual/index2.js +4 -0
  12. package/dist/_virtual/index2.js.map +1 -0
  13. package/dist/astro/index.d.ts +5 -0
  14. package/dist/astro/index.d.ts.map +1 -0
  15. package/dist/components.min.css +1 -0
  16. package/dist/css/button.module.css.js +4 -0
  17. package/dist/css/button.module.css.js.map +1 -0
  18. package/dist/css/card.module.css.js +4 -0
  19. package/dist/css/card.module.css.js.map +1 -0
  20. package/dist/css/index.d.ts +5 -0
  21. package/dist/css/index.d.ts.map +1 -0
  22. package/dist/css/tag.module.css.js +4 -0
  23. package/dist/css/tag.module.css.js.map +1 -0
  24. package/dist/css/theme-toggle.module.css.js +4 -0
  25. package/dist/css/theme-toggle.module.css.js.map +1 -0
  26. package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js +86 -0
  27. package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js.map +1 -0
  28. package/dist/react/Button/index.d.ts +6 -0
  29. package/dist/react/Button/index.d.ts.map +1 -0
  30. package/dist/react/Button/index.js +10 -0
  31. package/dist/react/Button/index.js.map +1 -0
  32. package/dist/react/Card/index.d.ts +6 -0
  33. package/dist/react/Card/index.d.ts.map +1 -0
  34. package/dist/react/Card/index.js +10 -0
  35. package/dist/react/Card/index.js.map +1 -0
  36. package/dist/react/Tag/index.d.ts +6 -0
  37. package/dist/react/Tag/index.d.ts.map +1 -0
  38. package/dist/react/Tag/index.js +10 -0
  39. package/dist/react/Tag/index.js.map +1 -0
  40. package/dist/react/ThemeToggle/index.d.ts +2 -0
  41. package/dist/react/ThemeToggle/index.d.ts.map +1 -0
  42. package/dist/react/ThemeToggle/index.js +25 -0
  43. package/dist/react/ThemeToggle/index.js.map +1 -0
  44. package/dist/react/index.d.ts +5 -0
  45. package/dist/react/index.d.ts.map +1 -0
  46. package/dist/react/index.js +5 -0
  47. package/dist/react/index.js.map +1 -0
  48. package/dist/shared/js/theme.d.ts +5 -0
  49. package/dist/shared/js/theme.d.ts.map +1 -0
  50. package/dist/shared/js/theme.js +22 -0
  51. package/dist/shared/js/theme.js.map +1 -0
  52. package/dist/shared/types/button.d.ts +8 -0
  53. package/dist/shared/types/button.d.ts.map +1 -0
  54. package/dist/shared/types/card.d.ts +5 -0
  55. package/dist/shared/types/card.d.ts.map +1 -0
  56. package/dist/shared/types/tag.d.ts +5 -0
  57. package/dist/shared/types/tag.d.ts.map +1 -0
  58. package/dist/shared/types/theme-toggle.d.ts +4 -0
  59. package/dist/shared/types/theme-toggle.d.ts.map +1 -0
  60. package/dist/vue/index.d.ts +5 -0
  61. package/dist/vue/index.d.ts.map +1 -0
  62. package/done/2026-03-03-design-system-components/osddt.plan.md +233 -0
  63. package/done/2026-03-03-design-system-components/osddt.spec.md +90 -0
  64. package/done/2026-03-03-design-system-components/osddt.tasks.md +100 -0
  65. package/package.json +76 -6
  66. package/rollup.config.mjs +32 -0
  67. package/setupTests.ts +1 -0
  68. package/src/astro/Button/index.astro +35 -0
  69. package/src/astro/Card/index.astro +23 -0
  70. package/src/astro/Tag/index.astro +23 -0
  71. package/src/astro/ThemeToggle/index.astro +63 -0
  72. package/src/astro/index.ts +4 -0
  73. package/src/css/button.module.css +90 -0
  74. package/src/css/card.module.css +30 -0
  75. package/src/css/index.ts +4 -0
  76. package/src/css/tag.module.css +33 -0
  77. package/src/css/theme-toggle.module.css +38 -0
  78. package/src/declarations.d.ts +19 -0
  79. package/src/react/Button/index.test.tsx +59 -0
  80. package/src/react/Button/index.tsx +31 -0
  81. package/src/react/Card/index.test.tsx +38 -0
  82. package/src/react/Card/index.tsx +14 -0
  83. package/src/react/Tag/index.test.tsx +35 -0
  84. package/src/react/Tag/index.tsx +14 -0
  85. package/src/react/ThemeToggle/index.test.tsx +84 -0
  86. package/src/react/ThemeToggle/index.tsx +36 -0
  87. package/src/react/index.ts +4 -0
  88. package/src/shared/js/theme.ts +22 -0
  89. package/src/shared/types/button.ts +8 -0
  90. package/src/shared/types/card.ts +5 -0
  91. package/src/shared/types/tag.ts +5 -0
  92. package/src/shared/types/theme-toggle.ts +5 -0
  93. package/src/vue/Button/index.vue +27 -0
  94. package/src/vue/Card/index.vue +18 -0
  95. package/src/vue/Tag/index.vue +18 -0
  96. package/src/vue/ThemeToggle/index.vue +39 -0
  97. package/src/vue/index.ts +4 -0
  98. package/tsconfig.json +19 -0
  99. package/vite.config.build.ts +34 -0
  100. package/vitest.config.ts +12 -0
@@ -0,0 +1,100 @@
1
+ # Task List: Design System Components
2
+
3
+ ---
4
+
5
+ ## Phase 1 — Package Setup
6
+
7
+ > All Phase 2–7 tasks depend on Phase 1 being complete.
8
+
9
+ - [x] [S] Configure `package.json`: add `exports` map for `./react`, `./astro`, `./vue` sub-paths, and add `vue` + `astro` as peer dependencies
10
+ - [x] [S] Create directory skeleton: `src/css/`, `src/shared/js/`, `src/shared/types/`, `src/react/`, `src/astro/`, `src/vue/`
11
+ - [x] [S] Create empty barrel files: `src/react/index.ts`, `src/astro/index.ts`, `src/vue/index.ts`
12
+ - [x] [S] Verify `@dezkareid/design-tokens` is installed and its CSS token file is importable; add as dependency if missing
13
+ - [x] [M] Set up Vitest config targeting `src/react/**/*.test.tsx` with jsdom environment and React Testing Library
14
+
15
+ **Definition of Done**: `package.json` has correct exports and peer deps; all directories exist; Vitest runs (no tests yet, zero failures).
16
+
17
+ ---
18
+
19
+ ## Phase 2 — Button Component
20
+
21
+ > Depends on: Phase 1
22
+
23
+ - [x] [S] Define `ButtonProps` interface in `src/shared/types/button.ts` (`variant`, `size`, `disabled`, `children`)
24
+ - [x] [M] Write `src/css/button.module.css` — structure (`.button`, size modifiers `.button--sm/md/lg`) and skin (variant modifiers `.button--primary/secondary`, state modifier `.button--disabled`) using BEM + OOCSS
25
+ - [x] [M] Implement `src/react/Button/index.tsx` — composes BEM classes, forwards native button props
26
+ - [x] [M] Write `src/react/Button/index.test.tsx` — renders primary/secondary variants, renders sm/md/lg sizes, disabled prevents click, has accessible button role
27
+ - [x] [M] Implement `src/astro/Button/index.astro`
28
+ - [x] [M] Implement `src/vue/Button/index.vue`
29
+
30
+ **Definition of Done**: Button renders all variants and sizes in all three frameworks; React tests pass; no hardcoded colour or spacing values.
31
+
32
+ ---
33
+
34
+ ## Phase 3 — Tag Component
35
+
36
+ > Depends on: Phase 1
37
+
38
+ - [x] [S] Define `TagProps` interface in `src/shared/types/tag.ts` (`variant`, `children`)
39
+ - [x] [M] Write `src/css/tag.module.css` — structure (`.tag`) and skin (variant modifiers `.tag--default/success/danger`) using BEM + OOCSS; add comment proposing `--color-danger` semantic token
40
+ - [x] [M] Implement `src/react/Tag/index.tsx` — renders `<span>`, accepts `children`, composes BEM classes
41
+ - [x] [M] Write `src/react/Tag/index.test.tsx` — renders default/success/danger variants, renders arbitrary children content
42
+ - [x] [M] Implement `src/astro/Tag/index.astro`
43
+ - [x] [M] Implement `src/vue/Tag/index.vue`
44
+
45
+ **Definition of Done**: Tag renders all variants with correct colours in all three frameworks; accepts arbitrary slot/children; React tests pass.
46
+
47
+ ---
48
+
49
+ ## Phase 4 — Card Component
50
+
51
+ > Depends on: Phase 1
52
+
53
+ - [x] [S] Define `CardProps` interface in `src/shared/types/card.ts` (`elevation`, `children`; default elevation `'raised'`)
54
+ - [x] [M] Write `src/css/card.module.css` — structure (`.card`) and skin (base skin on `.card`, elevation modifiers `.card--raised/flat`) using BEM + OOCSS; add comment proposing `--shadow-raised` token
55
+ - [x] [M] Implement `src/react/Card/index.tsx` — renders `<div>`, accepts `children`, composes BEM classes
56
+ - [x] [M] Write `src/react/Card/index.test.tsx` — renders children, applies `card--raised` by default, applies `card--flat` when set
57
+ - [x] [M] Implement `src/astro/Card/index.astro`
58
+ - [x] [M] Implement `src/vue/Card/index.vue`
59
+
60
+ **Definition of Done**: Card renders children inside a themed surface with correct elevation in all three frameworks; React tests pass.
61
+
62
+ ---
63
+
64
+ ## Phase 5 — ThemeToggle Component
65
+
66
+ > Depends on: Phase 1
67
+
68
+ - [x] [S] Define `ThemeToggleProps` interface in `src/shared/types/theme-toggle.ts` (no required props)
69
+ - [x] [M] Implement shared theme utilities in `src/shared/js/theme.ts`: `getInitialTheme()`, `applyTheme()`, `persistTheme()` — all SSR-safe with `typeof window` guards
70
+ - [x] [M] Write `src/css/theme-toggle.module.css` — structure (`.theme-toggle`) and skin + state modifier (`.theme-toggle--dark`) using BEM + OOCSS
71
+ - [x] [M] Implement `src/react/ThemeToggle/index.tsx` — uses `useState` + `useEffect`, calls shared theme utilities
72
+ - [x] [L] Write `src/react/ThemeToggle/index.test.tsx` — initialises from `localStorage`, falls back to `prefers-color-scheme`, toggles on click, persists to `localStorage`
73
+ - [x] [M] Implement `src/astro/ThemeToggle/index.astro` — static markup + inline `<script>` calling shared utilities
74
+ - [x] [M] Implement `src/vue/ThemeToggle/index.vue` — uses `ref` + `onMounted`, calls shared utilities
75
+
76
+ **Definition of Done**: ThemeToggle reads, applies, and persists theme in all three frameworks; React tests pass; no FOUC in Astro (inline script runs before paint).
77
+
78
+ ---
79
+
80
+ ## Phase 6 — Exports & Package Configuration
81
+
82
+ > Depends on: Phases 2–5
83
+
84
+ - [x] [S] Update `src/react/index.ts` to re-export `Button`, `Tag`, `Card`, `ThemeToggle`
85
+ - [x] [S] Update `src/astro/index.ts` to re-export all Astro components
86
+ - [x] [S] Update `src/vue/index.ts` to re-export all Vue components
87
+ - [x] [S] Verify `package.json` `exports` paths resolve correctly (smoke check imports)
88
+
89
+ **Definition of Done**: All components importable from `@dezkareid/components/react`, `/astro`, and `/vue`; no TypeScript errors.
90
+
91
+ ---
92
+
93
+ ## Phase 7 — Documentation
94
+
95
+ > Depends on: Phase 6
96
+
97
+ - [x] [M] Update `README.md`: add `@dezkareid/design-tokens` CSS import requirement, add usage examples for Button, Tag, Card, ThemeToggle in all three framework formats
98
+ - [x] [M] Update `AGENTS.md`: add component API summaries (props, variants, slot/children behaviour) for all four components
99
+
100
+ **Definition of Done**: README covers install, token CSS import, and usage for all components; AGENTS.md describes all component APIs for AI tooling.
package/package.json CHANGED
@@ -1,7 +1,28 @@
1
1
  {
2
2
  "name": "@dezkareid/components",
3
- "version": "0.0.0",
3
+ "version": "1.0.0",
4
+ "type": "module",
4
5
  "description": "A package to export UI components in formats like React, Astro, Vue, etc.",
6
+ "exports": {
7
+ "./react": {
8
+ "types": "./dist/react/index.d.ts",
9
+ "import": "./dist/react.js",
10
+ "default": "./dist/react.js"
11
+ },
12
+ "./astro": "./src/astro/index.ts",
13
+ "./vue": "./src/vue/index.ts",
14
+ "./css": {
15
+ "import": "./dist/components.min.css",
16
+ "default": "./dist/components.min.css"
17
+ }
18
+ },
19
+ "typesVersions": {
20
+ "*": {
21
+ "react": [
22
+ "./dist/react/index.d.ts"
23
+ ]
24
+ }
25
+ },
5
26
  "repository": {
6
27
  "type": "git",
7
28
  "url": "https://github.com/dezkareid/dezkareid"
@@ -15,8 +36,57 @@
15
36
  "access": "public"
16
37
  },
17
38
  "keywords": [
18
- "setup",
19
- "package",
20
- "npm"
21
- ]
22
- }
39
+ "design-system",
40
+ "components",
41
+ "react",
42
+ "astro",
43
+ "vue"
44
+ ],
45
+ "peerDependencies": {
46
+ "astro": ">=4.0.0",
47
+ "react": "^18.0.0 || ^19.0.0",
48
+ "react-dom": "^18.0.0 || ^19.0.0",
49
+ "vue": "^3.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "astro": {
53
+ "optional": true
54
+ },
55
+ "vue": {
56
+ "optional": true
57
+ },
58
+ "react": {
59
+ "optional": true
60
+ },
61
+ "react-dom": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "dependencies": {
66
+ "classnames": "2.5.1",
67
+ "@dezkareid/design-tokens": "0.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@rollup/plugin-commonjs": "29.0.0",
71
+ "@rollup/plugin-node-resolve": "16.0.3",
72
+ "@rollup/plugin-typescript": "12.3.0",
73
+ "@testing-library/jest-dom": "6.9.1",
74
+ "@testing-library/react": "16.3.2",
75
+ "@testing-library/user-event": "14.5.2",
76
+ "@types/classnames": "^2.3.4",
77
+ "@types/react": "19.2.9",
78
+ "@vitejs/plugin-react": "5.1.4",
79
+ "jsdom": "27.4.0",
80
+ "react": "19.2.4",
81
+ "react-dom": "19.2.4",
82
+ "rollup": "4.56.0",
83
+ "rollup-plugin-postcss": "4.0.2",
84
+ "typescript": "5.9.3",
85
+ "vite": "7.3.1",
86
+ "vitest": "4.0.18"
87
+ },
88
+ "scripts": {
89
+ "build": "rollup -c rollup.config.mjs",
90
+ "test": "vitest --run"
91
+ }
92
+ }
@@ -0,0 +1,32 @@
1
+ import typescript from '@rollup/plugin-typescript';
2
+ import resolve from '@rollup/plugin-node-resolve';
3
+ import commonjs from '@rollup/plugin-commonjs';
4
+ import postcss from 'rollup-plugin-postcss';
5
+
6
+ export default {
7
+ input: 'src/react/index.ts',
8
+ external: ['react', 'react/jsx-runtime'],
9
+ plugins: [
10
+ resolve({ extensions: ['.ts', '.tsx'] }),
11
+ commonjs(),
12
+ postcss({
13
+ autoModules: true,
14
+ extract: 'components.min.css',
15
+ minimize: true,
16
+ }),
17
+ typescript({
18
+ tsconfig: './tsconfig.json',
19
+ declaration: true,
20
+ declarationDir: 'dist',
21
+ exclude: ['**/*.test.tsx', '**/*.test.ts', 'setupTests.ts'],
22
+ }),
23
+ ],
24
+ output: {
25
+ dir: 'dist',
26
+ format: 'es',
27
+ preserveModules: true,
28
+ preserveModulesRoot: 'src',
29
+ entryFileNames: '[name].js',
30
+ sourcemap: true,
31
+ },
32
+ };
package/setupTests.ts ADDED
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -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
+ });