@dezkareid/components 1.0.0 → 1.0.2

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 (61) hide show
  1. package/dist/components.min.css +1 -1
  2. package/dist/css/tag.module.css.js +1 -1
  3. package/dist/css/theme-toggle.module.css.js +1 -1
  4. package/dist/react/Button/index.d.ts +3 -3
  5. package/dist/react/Button/index.d.ts.map +1 -1
  6. package/dist/react/Button/index.js +1 -1
  7. package/dist/react/Button/index.js.map +1 -1
  8. package/dist/react/Card/index.d.ts +3 -3
  9. package/dist/react/Card/index.d.ts.map +1 -1
  10. package/dist/react/Card/index.js.map +1 -1
  11. package/dist/react/Tag/index.d.ts +3 -3
  12. package/dist/react/Tag/index.d.ts.map +1 -1
  13. package/dist/react/Tag/index.js.map +1 -1
  14. package/dist/react/ThemeToggle/index.d.ts.map +1 -1
  15. package/dist/react/ThemeToggle/index.js +6 -2
  16. package/dist/react/ThemeToggle/index.js.map +1 -1
  17. package/dist/shared/js/theme.js +4 -4
  18. package/dist/shared/js/theme.js.map +1 -1
  19. package/dist/shared/types/button.d.ts +1 -1
  20. package/dist/shared/types/button.d.ts.map +1 -1
  21. package/dist/shared/types/card.d.ts +2 -1
  22. package/dist/shared/types/card.d.ts.map +1 -1
  23. package/dist/shared/types/tag.d.ts +2 -2
  24. package/dist/shared/types/tag.d.ts.map +1 -1
  25. package/dist/shared/types/theme-toggle.d.ts +1 -2
  26. package/dist/shared/types/theme-toggle.d.ts.map +1 -1
  27. package/package.json +18 -5
  28. package/src/astro/Button/index.astro +1 -1
  29. package/src/astro/ThemeToggle/index.astro +49 -11
  30. package/src/css/button.module.css +49 -14
  31. package/src/css/card.module.css +3 -1
  32. package/src/css/tag.module.css +21 -7
  33. package/src/css/theme-toggle.module.css +26 -0
  34. package/src/react/Button/index.test.tsx +17 -7
  35. package/src/react/Button/index.tsx +4 -3
  36. package/src/react/Card/index.test.tsx +13 -3
  37. package/src/react/Card/index.tsx +3 -3
  38. package/src/react/Tag/index.test.tsx +10 -0
  39. package/src/react/Tag/index.tsx +3 -3
  40. package/src/react/ThemeToggle/index.test.tsx +25 -2
  41. package/src/react/ThemeToggle/index.tsx +57 -9
  42. package/src/shared/js/theme.ts +4 -4
  43. package/src/shared/types/button.ts +1 -1
  44. package/src/shared/types/card.ts +2 -1
  45. package/src/shared/types/tag.ts +2 -2
  46. package/src/shared/types/theme-toggle.ts +1 -3
  47. package/src/vue/Button/index.vue +1 -1
  48. package/src/vue/ThemeToggle/index.vue +52 -9
  49. package/.releaserc +0 -18
  50. package/.turbo/turbo-build.log +0 -7
  51. package/.turbo/turbo-test.log +0 -17
  52. package/AGENTS.md +0 -174
  53. package/CHANGELOG.md +0 -12
  54. package/done/2026-03-03-design-system-components/osddt.plan.md +0 -233
  55. package/done/2026-03-03-design-system-components/osddt.spec.md +0 -90
  56. package/done/2026-03-03-design-system-components/osddt.tasks.md +0 -100
  57. package/rollup.config.mjs +0 -32
  58. package/setupTests.ts +0 -1
  59. package/tsconfig.json +0 -19
  60. package/vite.config.build.ts +0 -34
  61. package/vitest.config.ts +0 -12
@@ -36,3 +36,29 @@
36
36
  color: var(--color-primary);
37
37
  border-color: var(--color-primary);
38
38
  }
39
+
40
+ /* --- Structure: icon --- */
41
+ .theme-toggle__icon {
42
+ width: 1em;
43
+ height: 1em;
44
+ flex-shrink: 0;
45
+ }
46
+
47
+ /* --- Structure: wrapper --- */
48
+ .theme-toggle__wrapper {
49
+ display: inline-flex;
50
+ position: relative;
51
+ }
52
+
53
+ /* --- Utility: visually hidden live region --- */
54
+ .sr-only {
55
+ position: absolute;
56
+ width: 1px;
57
+ height: 1px;
58
+ padding: 0;
59
+ margin: -1px;
60
+ overflow: hidden;
61
+ clip: rect(0, 0, 0, 0);
62
+ white-space: nowrap;
63
+ border: 0;
64
+ }
@@ -11,14 +11,14 @@ describe('Button', () => {
11
11
 
12
12
  it('renders primary variant', () => {
13
13
  render(<Button variant="primary">Primary</Button>);
14
- const btn = screen.getByRole('button');
15
- expect(btn.className).toMatch(/button--primary/);
14
+ const button = screen.getByRole('button');
15
+ expect(button.className).toMatch(/button--primary/);
16
16
  });
17
17
 
18
18
  it('renders secondary variant', () => {
19
19
  render(<Button variant="secondary">Secondary</Button>);
20
- const btn = screen.getByRole('button');
21
- expect(btn.className).toMatch(/button--secondary/);
20
+ const button = screen.getByRole('button');
21
+ expect(button.className).toMatch(/button--secondary/);
22
22
  });
23
23
 
24
24
  it('renders sm size', () => {
@@ -38,9 +38,9 @@ describe('Button', () => {
38
38
 
39
39
  it('is disabled when disabled prop is set', () => {
40
40
  render(<Button disabled>Disabled</Button>);
41
- const btn = screen.getByRole('button');
42
- expect(btn).toBeDisabled();
43
- expect(btn.className).toMatch(/button--disabled/);
41
+ const button = screen.getByRole('button');
42
+ expect(button).toBeDisabled();
43
+ expect(button.className).toMatch(/button--disabled/);
44
44
  });
45
45
 
46
46
  it('does not fire onClick when disabled', async () => {
@@ -56,4 +56,14 @@ describe('Button', () => {
56
56
  await userEvent.click(screen.getByRole('button'));
57
57
  expect(onClick).toHaveBeenCalledOnce();
58
58
  });
59
+
60
+ it('has aria-disabled when disabled', () => {
61
+ render(<Button disabled>Disabled</Button>);
62
+ expect(screen.getByRole('button')).toHaveAttribute('aria-disabled', 'true');
63
+ });
64
+
65
+ it('does not have aria-disabled when enabled', () => {
66
+ render(<Button>Enabled</Button>);
67
+ expect(screen.getByRole('button')).toHaveAttribute('aria-disabled', 'false');
68
+ });
59
69
  });
@@ -1,9 +1,9 @@
1
1
  import type { ButtonHTMLAttributes } from 'react';
2
2
  import cx from 'classnames';
3
- import type { ButtonProps } from '../../shared/types/button';
3
+ import type { ButtonProperties } from '../../shared/types/button';
4
4
  import styles from '../../css/button.module.css';
5
5
 
6
- type Props = ButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
6
+ type Properties = ButtonProperties & ButtonHTMLAttributes<HTMLButtonElement>;
7
7
 
8
8
  export function Button({
9
9
  variant = 'primary',
@@ -12,7 +12,7 @@ export function Button({
12
12
  children,
13
13
  className,
14
14
  ...rest
15
- }: Props) {
15
+ }: Properties) {
16
16
  return (
17
17
  <button
18
18
  className={cx(
@@ -23,6 +23,7 @@ export function Button({
23
23
  className,
24
24
  )}
25
25
  disabled={disabled}
26
+ aria-disabled={disabled}
26
27
  {...rest}
27
28
  >
28
29
  {children}
@@ -26,13 +26,23 @@ describe('Card', () => {
26
26
 
27
27
  it('applies flat elevation when set', () => {
28
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/);
29
+ const element = container.firstChild as HTMLElement;
30
+ expect(element.className).toMatch(/card--flat/);
31
+ expect(element.className).not.toMatch(/card--raised/);
32
32
  });
33
33
 
34
34
  it('applies raised elevation when explicitly set', () => {
35
35
  const { container } = render(<Card elevation="raised">Content</Card>);
36
36
  expect((container.firstChild as HTMLElement).className).toMatch(/card--raised/);
37
37
  });
38
+
39
+ it('forwards role prop to root element', () => {
40
+ render(<Card role="article">Content</Card>);
41
+ expect(screen.getByRole('article')).toBeInTheDocument();
42
+ });
43
+
44
+ it('applies card--flat class when elevation is flat', () => {
45
+ const { container } = render(<Card elevation="flat">Content</Card>);
46
+ expect((container.firstChild as HTMLElement).className).toMatch(/card--flat/);
47
+ });
38
48
  });
@@ -1,11 +1,11 @@
1
1
  import type { HTMLAttributes } from 'react';
2
2
  import cx from 'classnames';
3
- import type { CardProps } from '../../shared/types/card';
3
+ import type { CardProperties } from '../../shared/types/card';
4
4
  import styles from '../../css/card.module.css';
5
5
 
6
- type Props = CardProps & HTMLAttributes<HTMLDivElement>;
6
+ type Properties = CardProperties & HTMLAttributes<HTMLDivElement>;
7
7
 
8
- export function Card({ elevation = 'raised', children, className, ...rest }: Props) {
8
+ export function Card({ elevation = 'raised', children, className, ...rest }: Properties) {
9
9
  return (
10
10
  <div className={cx(styles.card, styles[`card--${elevation}`], className)} {...rest}>
11
11
  {children}
@@ -32,4 +32,14 @@ describe('Tag', () => {
32
32
  render(<Tag>No variant</Tag>);
33
33
  expect(screen.getByText('No variant').className).toMatch(/tag--default/);
34
34
  });
35
+
36
+ it('renders warning variant', () => {
37
+ render(<Tag variant="warning">Warning</Tag>);
38
+ expect(screen.getByText('Warning').className).toMatch(/tag--warning/);
39
+ });
40
+
41
+ it('forwards aria-label to root span', () => {
42
+ render(<Tag variant="success" aria-label="Success status">Active</Tag>);
43
+ expect(screen.getByLabelText('Success status')).toBeInTheDocument();
44
+ });
35
45
  });
@@ -1,11 +1,11 @@
1
1
  import type { HTMLAttributes } from 'react';
2
2
  import cx from 'classnames';
3
- import type { TagProps } from '../../shared/types/tag';
3
+ import type { TagProperties } from '../../shared/types/tag';
4
4
  import styles from '../../css/tag.module.css';
5
5
 
6
- type Props = TagProps & HTMLAttributes<HTMLSpanElement>;
6
+ type Properties = TagProperties & HTMLAttributes<HTMLSpanElement>;
7
7
 
8
- export function Tag({ variant = 'default', children, className, ...rest }: Props) {
8
+ export function Tag({ variant = 'default', children, className, ...rest }: Properties) {
9
9
  return (
10
10
  <span className={cx(styles.tag, styles[`tag--${variant}`], className)} {...rest}>
11
11
  {children}
@@ -4,12 +4,12 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
4
4
  import { ThemeToggle } from './index';
5
5
 
6
6
  function mockMatchMedia(prefersDark: boolean) {
7
- Object.defineProperty(window, 'matchMedia', {
7
+ Object.defineProperty(globalThis, 'matchMedia', {
8
8
  writable: true,
9
9
  value: vi.fn((query: string) => ({
10
10
  matches: query === '(prefers-color-scheme: dark)' ? prefersDark : false,
11
11
  media: query,
12
- onchange: null,
12
+ onchange: undefined,
13
13
  addEventListener: vi.fn(),
14
14
  removeEventListener: vi.fn(),
15
15
  dispatchEvent: vi.fn(),
@@ -81,4 +81,27 @@ describe('ThemeToggle', () => {
81
81
  await act(async () => { render(<ThemeToggle />); });
82
82
  expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'false');
83
83
  });
84
+
85
+ it('has aria-label "Switch to dark mode" when in light mode', async () => {
86
+ await act(async () => { render(<ThemeToggle />); });
87
+ expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Switch to dark mode');
88
+ });
89
+
90
+ it('has aria-label "Switch to light mode" after toggling to dark', async () => {
91
+ await act(async () => { render(<ThemeToggle />); });
92
+ await userEvent.click(screen.getByRole('button'));
93
+ expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Switch to light mode');
94
+ });
95
+
96
+ it('renders an aria-live polite region', async () => {
97
+ await act(async () => { render(<ThemeToggle />); });
98
+ const liveRegion = document.querySelector('[aria-live="polite"]');
99
+ expect(liveRegion).toBeInTheDocument();
100
+ });
101
+
102
+ it('renders an SVG icon with aria-hidden', async () => {
103
+ await act(async () => { render(<ThemeToggle />); });
104
+ const svg = document.querySelector('svg[aria-hidden="true"]');
105
+ expect(svg).toBeInTheDocument();
106
+ });
84
107
  });
@@ -4,11 +4,52 @@ import type { Theme } from '../../shared/types/theme-toggle';
4
4
  import { getInitialTheme, applyTheme, persistTheme } from '../../shared/js/theme';
5
5
  import styles from '../../css/theme-toggle.module.css';
6
6
 
7
+ const SunIcon = (
8
+ <svg
9
+ aria-hidden="true"
10
+ className={styles['theme-toggle__icon']}
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ >
19
+ <circle cx="12" cy="12" r="5" />
20
+ <line x1="12" y1="1" x2="12" y2="3" />
21
+ <line x1="12" y1="21" x2="12" y2="23" />
22
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
23
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
24
+ <line x1="1" y1="12" x2="3" y2="12" />
25
+ <line x1="21" y1="12" x2="23" y2="12" />
26
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
27
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
28
+ </svg>
29
+ );
30
+
31
+ const MoonIcon = (
32
+ <svg
33
+ aria-hidden="true"
34
+ className={styles['theme-toggle__icon']}
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ >
43
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
44
+ </svg>
45
+ );
46
+
7
47
  export function ThemeToggle() {
8
48
  const [theme, setTheme] = useState<Theme>('light');
9
49
 
10
50
  useEffect(() => {
11
51
  const initial = getInitialTheme();
52
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
12
53
  setTheme(initial);
13
54
  applyTheme(initial);
14
55
  }, []);
@@ -21,16 +62,23 @@ export function ThemeToggle() {
21
62
  }
22
63
 
23
64
  const isDark = theme === 'dark';
65
+ const label = isDark ? 'Dark' : 'Light';
24
66
 
25
67
  return (
26
- <button
27
- type="button"
28
- className={cx(styles['theme-toggle'], isDark && styles['theme-toggle--dark'])}
29
- onClick={toggle}
30
- aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
31
- aria-pressed={isDark}
32
- >
33
- {isDark ? 'Dark' : 'Light'}
34
- </button>
68
+ <span className={styles['theme-toggle__wrapper']}>
69
+ <button
70
+ type="button"
71
+ className={cx(styles['theme-toggle'], isDark && styles['theme-toggle--dark'])}
72
+ onClick={toggle}
73
+ aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
74
+ aria-pressed={isDark}
75
+ >
76
+ {isDark ? MoonIcon : SunIcon}
77
+ {label}
78
+ </button>
79
+ <span aria-live="polite" className={styles['sr-only']}>
80
+ {label} mode active
81
+ </span>
82
+ </span>
35
83
  );
36
84
  }
@@ -3,20 +3,20 @@ import type { Theme } from '../types/theme-toggle';
3
3
  const STORAGE_KEY = 'color-scheme';
4
4
 
5
5
  export function getInitialTheme(): Theme {
6
- if (typeof window === 'undefined') return 'light';
6
+ if (globalThis.window === undefined) return 'light';
7
7
 
8
8
  const stored = localStorage.getItem(STORAGE_KEY);
9
9
  if (stored === 'light' || stored === 'dark') return stored;
10
10
 
11
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
11
+ return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
12
12
  }
13
13
 
14
14
  export function applyTheme(theme: Theme): void {
15
- if (typeof window === 'undefined') return;
15
+ if (globalThis.window === undefined) return;
16
16
  document.documentElement.setAttribute('color-scheme', theme);
17
17
  }
18
18
 
19
19
  export function persistTheme(theme: Theme): void {
20
- if (typeof window === 'undefined') return;
20
+ if (globalThis.window === undefined) return;
21
21
  localStorage.setItem(STORAGE_KEY, theme);
22
22
  }
@@ -1,7 +1,7 @@
1
1
  export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'success';
2
2
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large';
3
3
 
4
- export interface ButtonProps {
4
+ export interface ButtonProperties {
5
5
  variant?: ButtonVariant;
6
6
  size?: ButtonSize;
7
7
  disabled?: boolean;
@@ -1,5 +1,6 @@
1
1
  export type CardElevation = 'flat' | 'raised';
2
2
 
3
- export interface CardProps {
3
+ export interface CardProperties {
4
4
  elevation?: CardElevation;
5
+ role?: string;
5
6
  }
@@ -1,5 +1,5 @@
1
- export type TagVariant = 'default' | 'success' | 'danger';
1
+ export type TagVariant = 'default' | 'success' | 'danger' | 'warning';
2
2
 
3
- export interface TagProps {
3
+ export interface TagProperties {
4
4
  variant?: TagVariant;
5
5
  }
@@ -1,5 +1,3 @@
1
1
  export type Theme = 'light' | 'dark';
2
2
 
3
- export interface ThemeToggleProps {
4
- // No required props — self-contained stateful component
5
- }
3
+ export type ThemeToggleProperties = Record<never, never>;
@@ -21,7 +21,7 @@ const classes = computed(() =>
21
21
  </script>
22
22
 
23
23
  <template>
24
- <button :class="classes" :disabled="disabled">
24
+ <button :class="classes" :disabled="disabled" :aria-disabled="disabled">
25
25
  <slot />
26
26
  </button>
27
27
  </template>
@@ -27,13 +27,56 @@ const classes = () =>
27
27
  </script>
28
28
 
29
29
  <template>
30
- <button
31
- type="button"
32
- :class="classes()"
33
- :aria-label="isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
34
- :aria-pressed="isDark()"
35
- @click="toggle"
36
- >
37
- {{ isDark() ? 'Dark' : 'Light' }}
38
- </button>
30
+ <span :class="styles['theme-toggle__wrapper']">
31
+ <button
32
+ type="button"
33
+ :class="classes()"
34
+ :aria-label="isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
35
+ :aria-pressed="isDark()"
36
+ @click="toggle"
37
+ >
38
+ <!-- Moon icon (dark mode) -->
39
+ <svg
40
+ v-if="isDark()"
41
+ aria-hidden="true"
42
+ :class="styles['theme-toggle__icon']"
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ viewBox="0 0 24 24"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ stroke-width="2"
48
+ stroke-linecap="round"
49
+ stroke-linejoin="round"
50
+ >
51
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
52
+ </svg>
53
+ <!-- Sun icon (light mode) -->
54
+ <svg
55
+ v-else
56
+ aria-hidden="true"
57
+ :class="styles['theme-toggle__icon']"
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ viewBox="0 0 24 24"
60
+ fill="none"
61
+ stroke="currentColor"
62
+ stroke-width="2"
63
+ stroke-linecap="round"
64
+ stroke-linejoin="round"
65
+ >
66
+ <circle cx="12" cy="12" r="5" />
67
+ <line x1="12" y1="1" x2="12" y2="3" />
68
+ <line x1="12" y1="21" x2="12" y2="23" />
69
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
70
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
71
+ <line x1="1" y1="12" x2="3" y2="12" />
72
+ <line x1="21" y1="12" x2="23" y2="12" />
73
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
74
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
75
+ </svg>
76
+ {{ isDark() ? 'Dark' : 'Light' }}
77
+ </button>
78
+ <span aria-live="polite" :class="styles['sr-only']">
79
+ {{ isDark() ? 'Dark' : 'Light' }} mode active
80
+ </span>
81
+ </span>
39
82
  </template>
package/.releaserc DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "plugins": [
3
- "@semantic-release/commit-analyzer",
4
- "@semantic-release/release-notes-generator",
5
- "@semantic-release/changelog",
6
- "@anolilab/semantic-release-pnpm",
7
- [
8
- "@semantic-release/git",
9
- {
10
- "assets": [
11
- "CHANGELOG.md"
12
- ]
13
- }
14
- ],
15
- "@semantic-release/github"
16
- ],
17
- "extends": "semantic-release-monorepo"
18
- }
@@ -1,7 +0,0 @@
1
-
2
- > @dezkareid/components@0.0.0 build /home/runner/work/dezkareid/dezkareid/design-system/components
3
- > rollup -c rollup.config.mjs
4
-
5
- 
6
- src/react/index.ts → dist...
7
- created dist in 3.3s
@@ -1,17 +0,0 @@
1
-
2
- > @dezkareid/components@1.0.0 test /home/runner/work/dezkareid/dezkareid/design-system/components
3
- > vitest --run
4
-
5
-
6
-  RUN  v4.0.18 /home/runner/work/dezkareid/dezkareid/design-system/components
7
-
8
- ✓ src/react/Card/index.test.tsx (5 tests) 59ms
9
- ✓ src/react/Button/index.test.tsx (9 tests) 411ms
10
- ✓ src/react/ThemeToggle/index.test.tsx (9 tests) 436ms
11
- ✓ src/react/Tag/index.test.tsx (6 tests) 40ms
12
-
13
-  Test Files  4 passed (4)
14
-  Tests  29 passed (29)
15
-  Start at  17:01:39
16
-  Duration  2.42s (transform 223ms, setup 342ms, import 1.17s, tests 945ms, environment 2.76s)
17
-