@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.
- package/.releaserc +18 -0
- package/.turbo/turbo-build.log +7 -0
- package/.turbo/turbo-test.log +17 -0
- package/AGENTS.md +174 -0
- package/CHANGELOG.md +12 -0
- package/README.md +213 -5
- package/dist/_virtual/_commonjsHelpers.js +6 -0
- package/dist/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/_virtual/index.js +8 -0
- package/dist/_virtual/index.js.map +1 -0
- package/dist/_virtual/index2.js +4 -0
- package/dist/_virtual/index2.js.map +1 -0
- package/dist/astro/index.d.ts +5 -0
- package/dist/astro/index.d.ts.map +1 -0
- package/dist/components.min.css +1 -0
- package/dist/css/button.module.css.js +4 -0
- package/dist/css/button.module.css.js.map +1 -0
- package/dist/css/card.module.css.js +4 -0
- package/dist/css/card.module.css.js.map +1 -0
- package/dist/css/index.d.ts +5 -0
- package/dist/css/index.d.ts.map +1 -0
- package/dist/css/tag.module.css.js +4 -0
- package/dist/css/tag.module.css.js.map +1 -0
- package/dist/css/theme-toggle.module.css.js +4 -0
- package/dist/css/theme-toggle.module.css.js.map +1 -0
- package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js +86 -0
- package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js.map +1 -0
- package/dist/react/Button/index.d.ts +6 -0
- package/dist/react/Button/index.d.ts.map +1 -0
- package/dist/react/Button/index.js +10 -0
- package/dist/react/Button/index.js.map +1 -0
- package/dist/react/Card/index.d.ts +6 -0
- package/dist/react/Card/index.d.ts.map +1 -0
- package/dist/react/Card/index.js +10 -0
- package/dist/react/Card/index.js.map +1 -0
- package/dist/react/Tag/index.d.ts +6 -0
- package/dist/react/Tag/index.d.ts.map +1 -0
- package/dist/react/Tag/index.js +10 -0
- package/dist/react/Tag/index.js.map +1 -0
- package/dist/react/ThemeToggle/index.d.ts +2 -0
- package/dist/react/ThemeToggle/index.d.ts.map +1 -0
- package/dist/react/ThemeToggle/index.js +25 -0
- package/dist/react/ThemeToggle/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -0
- package/dist/shared/js/theme.d.ts +5 -0
- package/dist/shared/js/theme.d.ts.map +1 -0
- package/dist/shared/js/theme.js +22 -0
- package/dist/shared/js/theme.js.map +1 -0
- package/dist/shared/types/button.d.ts +8 -0
- package/dist/shared/types/button.d.ts.map +1 -0
- package/dist/shared/types/card.d.ts +5 -0
- package/dist/shared/types/card.d.ts.map +1 -0
- package/dist/shared/types/tag.d.ts +5 -0
- package/dist/shared/types/tag.d.ts.map +1 -0
- package/dist/shared/types/theme-toggle.d.ts +4 -0
- package/dist/shared/types/theme-toggle.d.ts.map +1 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/done/2026-03-03-design-system-components/osddt.plan.md +233 -0
- package/done/2026-03-03-design-system-components/osddt.spec.md +90 -0
- package/done/2026-03-03-design-system-components/osddt.tasks.md +100 -0
- package/package.json +76 -6
- package/rollup.config.mjs +32 -0
- package/setupTests.ts +1 -0
- package/src/astro/Button/index.astro +35 -0
- package/src/astro/Card/index.astro +23 -0
- package/src/astro/Tag/index.astro +23 -0
- package/src/astro/ThemeToggle/index.astro +63 -0
- package/src/astro/index.ts +4 -0
- package/src/css/button.module.css +90 -0
- package/src/css/card.module.css +30 -0
- package/src/css/index.ts +4 -0
- package/src/css/tag.module.css +33 -0
- package/src/css/theme-toggle.module.css +38 -0
- package/src/declarations.d.ts +19 -0
- package/src/react/Button/index.test.tsx +59 -0
- package/src/react/Button/index.tsx +31 -0
- package/src/react/Card/index.test.tsx +38 -0
- package/src/react/Card/index.tsx +14 -0
- package/src/react/Tag/index.test.tsx +35 -0
- package/src/react/Tag/index.tsx +14 -0
- package/src/react/ThemeToggle/index.test.tsx +84 -0
- package/src/react/ThemeToggle/index.tsx +36 -0
- package/src/react/index.ts +4 -0
- package/src/shared/js/theme.ts +22 -0
- package/src/shared/types/button.ts +8 -0
- package/src/shared/types/card.ts +5 -0
- package/src/shared/types/tag.ts +5 -0
- package/src/shared/types/theme-toggle.ts +5 -0
- package/src/vue/Button/index.vue +27 -0
- package/src/vue/Card/index.vue +18 -0
- package/src/vue/Tag/index.vue +18 -0
- package/src/vue/ThemeToggle/index.vue +39 -0
- package/src/vue/index.ts +4 -0
- package/tsconfig.json +19 -0
- package/vite.config.build.ts +34 -0
- package/vitest.config.ts +12 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { Tag } from './index';
|
|
4
|
+
|
|
5
|
+
describe('Tag', () => {
|
|
6
|
+
it('renders children content', () => {
|
|
7
|
+
render(<Tag>Label</Tag>);
|
|
8
|
+
expect(screen.getByText('Label')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders arbitrary children (elements)', () => {
|
|
12
|
+
render(<Tag><strong>Bold label</strong></Tag>);
|
|
13
|
+
expect(screen.getByText('Bold label')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders default variant', () => {
|
|
17
|
+
render(<Tag variant="default">Default</Tag>);
|
|
18
|
+
expect(screen.getByText('Default').className).toMatch(/tag--default/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders success variant', () => {
|
|
22
|
+
render(<Tag variant="success">Success</Tag>);
|
|
23
|
+
expect(screen.getByText('Success').className).toMatch(/tag--success/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders danger variant', () => {
|
|
27
|
+
render(<Tag variant="danger">Danger</Tag>);
|
|
28
|
+
expect(screen.getByText('Danger').className).toMatch(/tag--danger/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('defaults to default variant when no variant is provided', () => {
|
|
32
|
+
render(<Tag>No variant</Tag>);
|
|
33
|
+
expect(screen.getByText('No variant').className).toMatch(/tag--default/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import type { TagProps } from '../../shared/types/tag';
|
|
4
|
+
import styles from '../../css/tag.module.css';
|
|
5
|
+
|
|
6
|
+
type Props = TagProps & HTMLAttributes<HTMLSpanElement>;
|
|
7
|
+
|
|
8
|
+
export function Tag({ variant = 'default', children, className, ...rest }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<span className={cx(styles.tag, styles[`tag--${variant}`], className)} {...rest}>
|
|
11
|
+
{children}
|
|
12
|
+
</span>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
4
|
+
import { ThemeToggle } from './index';
|
|
5
|
+
|
|
6
|
+
function mockMatchMedia(prefersDark: boolean) {
|
|
7
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
8
|
+
writable: true,
|
|
9
|
+
value: vi.fn((query: string) => ({
|
|
10
|
+
matches: query === '(prefers-color-scheme: dark)' ? prefersDark : false,
|
|
11
|
+
media: query,
|
|
12
|
+
onchange: null,
|
|
13
|
+
addEventListener: vi.fn(),
|
|
14
|
+
removeEventListener: vi.fn(),
|
|
15
|
+
dispatchEvent: vi.fn(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
localStorage.clear();
|
|
22
|
+
document.documentElement.removeAttribute('color-scheme');
|
|
23
|
+
mockMatchMedia(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('ThemeToggle', () => {
|
|
27
|
+
it('renders a button', () => {
|
|
28
|
+
render(<ThemeToggle />);
|
|
29
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('initialises to light when localStorage is empty and OS preference is light', async () => {
|
|
33
|
+
mockMatchMedia(false);
|
|
34
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
35
|
+
expect(screen.getByRole('button')).toHaveTextContent('Light');
|
|
36
|
+
expect(document.documentElement.getAttribute('color-scheme')).toBe('light');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('initialises to dark from OS preference when localStorage is empty', async () => {
|
|
40
|
+
mockMatchMedia(true);
|
|
41
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
42
|
+
expect(screen.getByRole('button')).toHaveTextContent('Dark');
|
|
43
|
+
expect(document.documentElement.getAttribute('color-scheme')).toBe('dark');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('initialises from localStorage over OS preference', async () => {
|
|
47
|
+
localStorage.setItem('color-scheme', 'dark');
|
|
48
|
+
mockMatchMedia(false);
|
|
49
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
50
|
+
expect(screen.getByRole('button')).toHaveTextContent('Dark');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('toggles from light to dark on click', async () => {
|
|
54
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
55
|
+
await userEvent.click(screen.getByRole('button'));
|
|
56
|
+
expect(screen.getByRole('button')).toHaveTextContent('Dark');
|
|
57
|
+
expect(document.documentElement.getAttribute('color-scheme')).toBe('dark');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('toggles from dark to light on click', async () => {
|
|
61
|
+
localStorage.setItem('color-scheme', 'dark');
|
|
62
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
63
|
+
await userEvent.click(screen.getByRole('button'));
|
|
64
|
+
expect(screen.getByRole('button')).toHaveTextContent('Light');
|
|
65
|
+
expect(document.documentElement.getAttribute('color-scheme')).toBe('light');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('persists theme to localStorage on toggle', async () => {
|
|
69
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
70
|
+
await userEvent.click(screen.getByRole('button'));
|
|
71
|
+
expect(localStorage.getItem('color-scheme')).toBe('dark');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('sets aria-pressed to true when dark', async () => {
|
|
75
|
+
localStorage.setItem('color-scheme', 'dark');
|
|
76
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
77
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('sets aria-pressed to false when light', async () => {
|
|
81
|
+
await act(async () => { render(<ThemeToggle />); });
|
|
82
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'false');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import type { Theme } from '../../shared/types/theme-toggle';
|
|
4
|
+
import { getInitialTheme, applyTheme, persistTheme } from '../../shared/js/theme';
|
|
5
|
+
import styles from '../../css/theme-toggle.module.css';
|
|
6
|
+
|
|
7
|
+
export function ThemeToggle() {
|
|
8
|
+
const [theme, setTheme] = useState<Theme>('light');
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const initial = getInitialTheme();
|
|
12
|
+
setTheme(initial);
|
|
13
|
+
applyTheme(initial);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
function toggle() {
|
|
17
|
+
const next: Theme = theme === 'light' ? 'dark' : 'light';
|
|
18
|
+
setTheme(next);
|
|
19
|
+
applyTheme(next);
|
|
20
|
+
persistTheme(next);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isDark = theme === 'dark';
|
|
24
|
+
|
|
25
|
+
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>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Theme } from '../types/theme-toggle';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'color-scheme';
|
|
4
|
+
|
|
5
|
+
export function getInitialTheme(): Theme {
|
|
6
|
+
if (typeof window === 'undefined') return 'light';
|
|
7
|
+
|
|
8
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
9
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
10
|
+
|
|
11
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function applyTheme(theme: Theme): void {
|
|
15
|
+
if (typeof window === 'undefined') return;
|
|
16
|
+
document.documentElement.setAttribute('color-scheme', theme);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function persistTheme(theme: Theme): void {
|
|
20
|
+
if (typeof window === 'undefined') return;
|
|
21
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'success';
|
|
2
|
+
export type ButtonSize = 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large';
|
|
3
|
+
|
|
4
|
+
export interface ButtonProps {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ButtonSize;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import type { ButtonProps } from '../../shared/types/button';
|
|
5
|
+
import styles from '../../css/button.module.css';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<ButtonProps>(), {
|
|
8
|
+
variant: 'primary',
|
|
9
|
+
size: 'md',
|
|
10
|
+
disabled: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const classes = computed(() =>
|
|
14
|
+
cx(
|
|
15
|
+
styles.button,
|
|
16
|
+
styles[`button--${props.variant}`],
|
|
17
|
+
styles[`button--${props.size}`],
|
|
18
|
+
props.disabled && styles['button--disabled'],
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<button :class="classes" :disabled="disabled">
|
|
25
|
+
<slot />
|
|
26
|
+
</button>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import type { CardProps } from '../../shared/types/card';
|
|
5
|
+
import styles from '../../css/card.module.css';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<CardProps>(), {
|
|
8
|
+
elevation: 'raised',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const classes = computed(() => cx(styles.card, styles[`card--${props.elevation}`]));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div :class="classes">
|
|
16
|
+
<slot />
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import type { TagProps } from '../../shared/types/tag';
|
|
5
|
+
import styles from '../../css/tag.module.css';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<TagProps>(), {
|
|
8
|
+
variant: 'default',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const classes = computed(() => cx(styles.tag, styles[`tag--${props.variant}`]));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<span :class="classes">
|
|
16
|
+
<slot />
|
|
17
|
+
</span>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import type { Theme } from '../../shared/types/theme-toggle';
|
|
5
|
+
import { getInitialTheme, applyTheme, persistTheme } from '../../shared/js/theme';
|
|
6
|
+
import styles from '../../css/theme-toggle.module.css';
|
|
7
|
+
|
|
8
|
+
const theme = ref<Theme>('light');
|
|
9
|
+
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
const initial = getInitialTheme();
|
|
12
|
+
theme.value = initial;
|
|
13
|
+
applyTheme(initial);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function toggle() {
|
|
17
|
+
const next: Theme = theme.value === 'light' ? 'dark' : 'light';
|
|
18
|
+
theme.value = next;
|
|
19
|
+
applyTheme(next);
|
|
20
|
+
persistTheme(next);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isDark = () => theme.value === 'dark';
|
|
24
|
+
|
|
25
|
+
const classes = () =>
|
|
26
|
+
cx(styles['theme-toggle'], isDark() && styles['theme-toggle--dark']);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
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>
|
|
39
|
+
</template>
|
package/src/vue/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
|
16
|
+
"outDir": "dist"
|
|
17
|
+
},
|
|
18
|
+
"include": ["src", "setupTests.ts"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import type { Plugin } from 'vite';
|
|
5
|
+
|
|
6
|
+
function stripCssImports(): Plugin {
|
|
7
|
+
return {
|
|
8
|
+
name: 'strip-css-imports',
|
|
9
|
+
transform(code, id) {
|
|
10
|
+
if (!id.endsWith('.ts') && !id.endsWith('.tsx')) return;
|
|
11
|
+
return code.replace(/^import\s+\w+\s+from\s+['"].*?\.module\.css['"];?\s*$/gm, '');
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
plugins: [react(), stripCssImports()],
|
|
18
|
+
build: {
|
|
19
|
+
lib: {
|
|
20
|
+
entry: { react: resolve(__dirname, 'src/react/index.ts') },
|
|
21
|
+
formats: ['es'],
|
|
22
|
+
},
|
|
23
|
+
rollupOptions: {
|
|
24
|
+
external: ['react', 'react/jsx-runtime', /\.css$/],
|
|
25
|
+
output: {
|
|
26
|
+
preserveModules: true,
|
|
27
|
+
preserveModulesRoot: 'src',
|
|
28
|
+
entryFileNames: '[name].js',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
outDir: 'dist',
|
|
32
|
+
emptyOutDir: true,
|
|
33
|
+
},
|
|
34
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
test: {
|
|
7
|
+
globals: true,
|
|
8
|
+
environment: 'jsdom',
|
|
9
|
+
setupFiles: './setupTests.ts',
|
|
10
|
+
include: ['src/react/**/*.test.{ts,tsx}'],
|
|
11
|
+
},
|
|
12
|
+
});
|