@slyxup/ui 0.1.0 → 0.2.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 (74) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +27 -0
  3. package/LICENSE +21 -0
  4. package/README.md +105 -0
  5. package/dist/components/BillingPortal/BillingPortal.d.ts +22 -0
  6. package/dist/components/BillingPortal/BillingPortal.d.ts.map +1 -0
  7. package/dist/components/BillingPortal/BillingPortal.js +59 -0
  8. package/dist/components/BillingPortal/BillingPortal.js.map +1 -0
  9. package/dist/components/EmailVerification/EmailVerification.d.ts +9 -0
  10. package/dist/components/EmailVerification/EmailVerification.d.ts.map +1 -0
  11. package/dist/components/EmailVerification/EmailVerification.js +72 -0
  12. package/dist/components/EmailVerification/EmailVerification.js.map +1 -0
  13. package/dist/components/ForgotPassword/ForgotPassword.d.ts +8 -0
  14. package/dist/components/ForgotPassword/ForgotPassword.d.ts.map +1 -0
  15. package/dist/components/ForgotPassword/ForgotPassword.js +43 -0
  16. package/dist/components/ForgotPassword/ForgotPassword.js.map +1 -0
  17. package/dist/components/PricingTable/PricingTable.d.ts +19 -0
  18. package/dist/components/PricingTable/PricingTable.d.ts.map +1 -0
  19. package/dist/components/PricingTable/PricingTable.js +52 -0
  20. package/dist/components/PricingTable/PricingTable.js.map +1 -0
  21. package/dist/components/ResetPassword/ResetPassword.d.ts +9 -0
  22. package/dist/components/ResetPassword/ResetPassword.d.ts.map +1 -0
  23. package/dist/components/ResetPassword/ResetPassword.js +50 -0
  24. package/dist/components/ResetPassword/ResetPassword.js.map +1 -0
  25. package/dist/components/SignIn/SignIn.d.ts +11 -0
  26. package/dist/components/SignIn/SignIn.d.ts.map +1 -0
  27. package/dist/components/SignIn/SignIn.js +53 -0
  28. package/dist/components/SignIn/SignIn.js.map +1 -0
  29. package/dist/components/SignUp/SignUp.d.ts +8 -0
  30. package/dist/components/SignUp/SignUp.d.ts.map +1 -0
  31. package/dist/components/SignUp/SignUp.js +54 -0
  32. package/dist/components/SignUp/SignUp.js.map +1 -0
  33. package/dist/components/SocialButtons/SocialButtons.d.ts +9 -0
  34. package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -0
  35. package/dist/components/SocialButtons/SocialButtons.js +16 -0
  36. package/dist/components/SocialButtons/SocialButtons.js.map +1 -0
  37. package/dist/components/UserButton/UserButton.d.ts +3 -0
  38. package/dist/components/UserButton/UserButton.d.ts.map +1 -0
  39. package/dist/components/UserButton/UserButton.js +43 -0
  40. package/dist/components/UserButton/UserButton.js.map +1 -0
  41. package/dist/components/UserProfile/UserProfile.d.ts +3 -0
  42. package/dist/components/UserProfile/UserProfile.d.ts.map +1 -0
  43. package/dist/components/UserProfile/UserProfile.js +38 -0
  44. package/dist/components/UserProfile/UserProfile.js.map +1 -0
  45. package/dist/icons.d.ts +6 -0
  46. package/dist/icons.d.ts.map +1 -0
  47. package/dist/icons.js +15 -0
  48. package/dist/icons.js.map +1 -0
  49. package/dist/index.d.ts +15 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +22 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/styles.d.ts +9 -0
  54. package/dist/styles.d.ts.map +1 -0
  55. package/dist/styles.js +219 -0
  56. package/dist/styles.js.map +1 -0
  57. package/package.json +35 -3
  58. package/src/components/BillingPortal/BillingPortal.tsx +182 -0
  59. package/src/components/EmailVerification/EmailVerification.tsx +180 -0
  60. package/src/components/ForgotPassword/ForgotPassword.tsx +122 -0
  61. package/src/components/PricingTable/PricingTable.tsx +155 -0
  62. package/src/components/ResetPassword/ResetPassword.tsx +120 -0
  63. package/src/components/SignIn/SignIn.tsx +168 -0
  64. package/src/components/SignUp/SignUp.tsx +181 -0
  65. package/src/components/SocialButtons/SocialButtons.tsx +39 -0
  66. package/src/components/UserButton/UserButton.tsx +89 -0
  67. package/src/components/UserProfile/UserProfile.tsx +93 -0
  68. package/src/icons.tsx +71 -0
  69. package/src/index.ts +37 -1
  70. package/src/styles.ts +221 -0
  71. package/test/components.test.tsx +135 -0
  72. package/test/icons.test.tsx +32 -0
  73. package/test/styles.test.ts +47 -0
  74. package/vitest.config.ts +2 -0
package/src/styles.ts ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * SlyxUp UI — design tokens + stylesheet.
3
+ * Injected once via <SlyxUpStyles />. Theme with CSS variables on :root or .slyxup-scope.
4
+ */
5
+
6
+ export const FONT_LINK = `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">`;
7
+
8
+ export const CSS = `
9
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap');
10
+
11
+ .slyxup-root {
12
+ --slx-accent: #5b5bd6;
13
+ --slx-accent-hover: #4c4cc4;
14
+ --slx-accent-soft: rgba(91, 91, 214, 0.14);
15
+ --slx-bg: #ffffff;
16
+ --slx-bg-page: #e9eaf6;
17
+ --slx-ink: #16161d;
18
+ --slx-muted: #6f6f7b;
19
+ --slx-border: #d8d8e8;
20
+ --slx-danger: #d64550;
21
+ --slx-radius: 12px;
22
+ --slx-font: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
23
+ --slx-display: "Space Grotesk", "DM Sans", sans-serif;
24
+ --slx-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
25
+
26
+ font-family: var(--slx-font);
27
+ color: var(--slx-ink);
28
+ -webkit-font-smoothing: antialiased;
29
+ }
30
+ @media (prefers-color-scheme: dark) {
31
+ .slyxup-root:not(.slyxup-light) {
32
+ --slx-accent: #8484f2;
33
+ --slx-accent-hover: #9696f5;
34
+ --slx-accent-soft: rgba(132, 132, 242, 0.14);
35
+ --slx-bg: #191920;
36
+ --slx-bg-page: #121218;
37
+ --slx-ink: #f2f2f5;
38
+ --slx-muted: #9a9aa6;
39
+ --slx-border: #2c2c36;
40
+ --slx-danger: #f0737d;
41
+ }
42
+ }
43
+
44
+ @keyframes slx-shake {
45
+ 10%, 90% { transform: translateX(-1px); }
46
+ 20%, 80% { transform: translateX(2px); }
47
+ 30%, 50%, 70% { transform: translateX(-4px); }
48
+ 40%, 60% { transform: translateX(4px); }
49
+ }
50
+ @keyframes slx-spin { to { transform: rotate(360deg); } }
51
+ @media (prefers-reduced-motion: reduce) {
52
+ .slyxup-root * { animation: none !important; transition: none !important; }
53
+ }
54
+
55
+ /* ── Card ── */
56
+ .slx-card {
57
+ width: 100%;
58
+ max-width: 380px;
59
+ background: var(--slx-bg);
60
+ border: 1px solid var(--slx-border);
61
+ border-radius: calc(var(--slx-radius) + 4px);
62
+ padding: 32px;
63
+ box-shadow: 0 4px 12px rgba(16,16,29,.08), 0 16px 40px rgba(16,16,29,.12), 0 0 0 1px rgba(16,16,29,.04);
64
+ box-sizing: border-box;
65
+ }
66
+ .slyxup-root .slx-card { background: #ffffff; }
67
+ .slx-card-error { animation: slx-shake .45s cubic-bezier(.36,.07,.19,.97) both; }
68
+
69
+ /* ── Header / keyhole mark ── */
70
+ .slx-mark {
71
+ width: 44px; height: 44px; border-radius: 12px;
72
+ display: flex; align-items: center; justify-content: center;
73
+ background: linear-gradient(135deg, var(--slx-accent), #9a6cf0);
74
+ margin-bottom: 18px;
75
+ }
76
+ .slx-mark svg { display: block; }
77
+ .slx-title { font-family: var(--slx-display); font-size: 20px; font-weight: 650; letter-spacing: -0.02em; margin: 0 0 6px; }
78
+ .slx-subtitle { font-size: 13.5px; color: var(--slx-muted); margin: 0 0 22px; line-height: 1.5; }
79
+
80
+ /* ── Fields ── */
81
+ .slx-field { margin-bottom: 14px; }
82
+ .slx-label { display: block; font-size: 12.5px; font-weight: 550; margin-bottom: 6px; letter-spacing: 0.01em; }
83
+ .slx-input {
84
+ width: 100%; box-sizing: border-box;
85
+ font: inherit; font-size: 14px; color: var(--slx-ink);
86
+ background: transparent;
87
+ border: 1px solid var(--slx-border);
88
+ border-radius: var(--slx-radius);
89
+ padding: 10px 12px;
90
+ outline: none;
91
+ transition: border-color .15s, box-shadow .15s;
92
+ }
93
+ .slx-input::placeholder { color: var(--slx-muted); opacity: .75; }
94
+ .slx-input:focus-visible {
95
+ border-color: var(--slx-accent);
96
+ box-shadow: 0 0 0 3px var(--slx-accent-soft);
97
+ }
98
+ .slx-hint { font-size: 12px; color: var(--slx-muted); margin-top: 5px; }
99
+ .slx-error-text {
100
+ font-size: 13px; color: var(--slx-danger);
101
+ background: color-mix(in srgb, var(--slx-danger) 9%, transparent);
102
+ border-radius: 8px; padding: 9px 11px; margin: 0 0 14px; line-height: 1.4;
103
+ }
104
+
105
+ /* ── Button ── */
106
+ .slx-btn {
107
+ width: 100%; box-sizing: border-box;
108
+ font-family: var(--slx-font); font-size: 14px; font-weight: 600; letter-spacing: 0.01em;
109
+ color: #fff; background: #0a0a0f;
110
+ border: 1px solid #0a0a0f; border-radius: var(--slx-radius);
111
+ padding: 11px 14px; cursor: pointer;
112
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
113
+ transition: background .15s, transform .06s, border-color .15s;
114
+ }
115
+ .slx-btn:hover { background: #1a1a23; border-color: #1a1a23; }
116
+ .slx-btn:active { transform: scale(.985); background: #000; }
117
+ .slx-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(10,10,15,.14), 0 0 0 1px #0a0a0f; }
118
+ .slx-btn[disabled] { opacity: .6; cursor: not-allowed; }
119
+ @media (prefers-color-scheme: dark) {
120
+ .slyxup-root:not(.slyxup-light) .slx-btn { background: #f2f2f5; color: #0a0a0f; border-color: #f2f2f5; }
121
+ .slyxup-root:not(.slyxup-light) .slx-btn:hover { background: #e6e6eb; border-color: #e6e6eb; }
122
+ }
123
+ .slx-spinner {
124
+ width: 15px; height: 15px; flex: none;
125
+ border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
126
+ border-radius: 50%; animation: slx-spin .7s linear infinite;
127
+ }
128
+
129
+ /* ── Social ── */
130
+ .slx-social { display: grid; gap: 10px; margin-bottom: 16px; }
131
+ .slx-social-btn {
132
+ width: 100%; box-sizing: border-box;
133
+ font: inherit; font-size: 13.5px; font-weight: 550;
134
+ color: var(--slx-ink); background: var(--slx-bg);
135
+ border: 1px solid var(--slx-border); border-radius: var(--slx-radius);
136
+ padding: 10px 14px; cursor: pointer;
137
+ display: inline-flex; align-items: center; justify-content: center; gap: 10px;
138
+ transition: background .15s, border-color .15s;
139
+ }
140
+ .slx-social-btn:hover { background: color-mix(in srgb, var(--slx-ink) 4%, var(--slx-bg)); }
141
+ .slx-social-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--slx-accent-soft); }
142
+
143
+ /* ── Divider & footer ── */
144
+ .slx-divider {
145
+ display: flex; align-items: center; gap: 12px;
146
+ color: var(--slx-muted); font-size: 12px;
147
+ margin: 18px 0;
148
+ }
149
+ .slx-divider::before, .slx-divider::after {
150
+ content: ""; height: 1px; flex: 1; background: var(--slx-border);
151
+ }
152
+ .slx-footer { font-size: 13px; color: var(--slx-muted); margin-top: 20px; text-align: center; }
153
+ .slx-link {
154
+ color: var(--slx-accent); font-weight: 600; text-decoration: none; cursor: pointer;
155
+ background: none; border: none; font: inherit; font-size: inherit;
156
+ padding: 0; margin: 0;
157
+ }
158
+ .slx-link:hover { text-decoration: underline; color: var(--slx-accent-hover); }
159
+ .slx-link:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--slx-accent-soft); border-radius: 4px; }
160
+
161
+ /* ── Success state ── */
162
+ .slx-success-icon {
163
+ width: 46px; height: 46px; border-radius: 50%;
164
+ display: flex; align-items: center; justify-content: center;
165
+ background: color-mix(in srgb, #34a853 12%, transparent);
166
+ color: #34a853; margin: 4px auto 18px;
167
+ }
168
+
169
+ /* ── User button ── */
170
+ .slx-userbtn-wrap { position: relative; display: inline-block; }
171
+ .slx-userbtn-avatar {
172
+ width: 36px; height: 36px; border-radius: 50%;
173
+ border: 1px solid var(--slx-border); cursor: pointer;
174
+ display: flex; align-items: center; justify-content: center;
175
+ font-size: 14px; font-weight: 650; color: #fff;
176
+ background: linear-gradient(135deg, var(--slx-accent), #9a6cf0);
177
+ padding: 0; overflow: hidden;
178
+ }
179
+ .slx-userbtn-avatar img { width: 100%; height: 100%; object-fit: cover; }
180
+ .slx-userbtn-avatar:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--slx-accent-soft); }
181
+ .slx-menu {
182
+ position: absolute; right: 0; top: calc(100% + 8px);
183
+ min-width: 230px;
184
+ background: var(--slx-bg);
185
+ border: 1px solid var(--slx-border);
186
+ border-radius: var(--slx-radius);
187
+ box-shadow: 0 8px 30px rgba(10,10,20,.14);
188
+ overflow: hidden; z-index: 1000;
189
+ }
190
+ .slx-menu-header { padding: 14px 16px; border-bottom: 1px solid var(--slx-border); }
191
+ .slx-menu-name { font-size: 14px; font-weight: 600; margin: 0 0 2px; }
192
+ .slx-menu-email { font-size: 12.5px; color: var(--slx-muted); margin: 0; word-break: break-all; }
193
+ .slx-menu-item {
194
+ display: block; width: 100%; text-align: left; box-sizing: border-box;
195
+ font: inherit; font-size: 13.5px; color: var(--slx-ink);
196
+ background: none; border: none; padding: 10px 16px; cursor: pointer;
197
+ }
198
+ .slx-menu-item:hover { background: color-mix(in srgb, var(--slx-ink) 5%, transparent); }
199
+ .slx-menu-item:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--slx-accent-soft); }
200
+ .slx-menu-item-danger { color: var(--slx-danger); }
201
+ `;
202
+
203
+ let injected = false;
204
+
205
+ /** Inject the SlyxUp stylesheet + DM Sans font once per document. */
206
+ export function injectStyles(): void {
207
+ if (injected || typeof document === 'undefined') return;
208
+ // DM Sans font link
209
+ if (!document.querySelector('link[href*="DM+Sans"]')) {
210
+ const fontLink = document.createElement('link');
211
+ fontLink.rel = 'stylesheet';
212
+ fontLink.href =
213
+ 'https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap';
214
+ document.head.appendChild(fontLink);
215
+ }
216
+ const style = document.createElement('style');
217
+ style.setAttribute('data-slyxup', 'styles');
218
+ style.textContent = CSS;
219
+ document.head.appendChild(style);
220
+ injected = true;
221
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import React from 'react';
4
+
5
+ const mockSignIn = vi.fn().mockResolvedValue({ ok: true });
6
+ const mockSignUp = vi.fn().mockResolvedValue({ ok: true });
7
+ const mockSignOut = vi.fn().mockResolvedValue({ ok: true });
8
+
9
+ vi.mock('@slyxup/react', () => ({
10
+ useAuth: () => ({
11
+ isLoaded: true,
12
+ isSignedIn: false,
13
+ userId: null,
14
+ client: {},
15
+ signIn: mockSignIn,
16
+ signUp: mockSignUp,
17
+ signOut: mockSignOut,
18
+ }),
19
+ useUser: () => ({
20
+ isLoaded: true,
21
+ isSignedIn: false,
22
+ user: null,
23
+ isSignedOut: true,
24
+ reload: vi.fn(),
25
+ }),
26
+ useSession: () => ({
27
+ isLoaded: true,
28
+ isSignedIn: false,
29
+ session: null,
30
+ reload: vi.fn(),
31
+ }),
32
+ }));
33
+
34
+ import { SignIn } from '../src/components/SignIn/SignIn';
35
+ import { SignUp } from '../src/components/SignUp/SignUp';
36
+ import { SocialButtons } from '../src/components/SocialButtons/SocialButtons';
37
+ import { UserButton } from '../src/components/UserButton/UserButton';
38
+
39
+ describe('SignIn', () => {
40
+ beforeEach(() => vi.clearAllMocks());
41
+
42
+ it('renders sign in heading', () => {
43
+ render(<SignIn />);
44
+ expect(screen.getByRole('heading', { name: /sign in/i })).toBeTruthy();
45
+ });
46
+
47
+ it('renders email and password inputs', () => {
48
+ render(<SignIn />);
49
+ expect(screen.getByLabelText(/email/i)).toBeTruthy();
50
+ expect(screen.getByLabelText(/password/i)).toBeTruthy();
51
+ });
52
+
53
+ it('renders submit button', () => {
54
+ render(<SignIn />);
55
+ expect(screen.getByRole('button', { name: /sign in/i })).toBeTruthy();
56
+ });
57
+
58
+ it('shows social buttons by default', () => {
59
+ render(<SignIn />);
60
+ expect(screen.getByText(/continue with google/i)).toBeTruthy();
61
+ expect(screen.getByText(/continue with github/i)).toBeTruthy();
62
+ });
63
+
64
+ it('hides social buttons when social=false', () => {
65
+ render(<SignIn social={false} />);
66
+ expect(screen.queryByText(/continue with google/i)).toBeNull();
67
+ expect(screen.queryByText(/continue with github/i)).toBeNull();
68
+ });
69
+
70
+ it('calls signIn on form submit', async () => {
71
+ render(<SignIn />);
72
+ fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'a@b.com' } });
73
+ fireEvent.change(screen.getByLabelText(/password/i), { target: { value: '12345678' } });
74
+ fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
75
+ await waitFor(() => expect(mockSignIn).toHaveBeenCalledWith({ email: 'a@b.com', password: '12345678' }));
76
+ });
77
+
78
+ it('shows sign up link when onSignUpClick provided', () => {
79
+ const onSignUp = vi.fn();
80
+ render(<SignIn onSignUpClick={onSignUp} />);
81
+ expect(screen.getByText(/sign up/i)).toBeTruthy();
82
+ });
83
+ });
84
+
85
+ describe('SignUp', () => {
86
+ beforeEach(() => vi.clearAllMocks());
87
+
88
+ it('renders create your account heading', () => {
89
+ render(<SignUp />);
90
+ expect(screen.getByRole('heading', { name: /create your account/i })).toBeTruthy();
91
+ });
92
+
93
+ it('renders first name, email, password inputs', () => {
94
+ render(<SignUp />);
95
+ expect(screen.getByLabelText(/first name/i)).toBeTruthy();
96
+ expect(screen.getByLabelText(/email/i)).toBeTruthy();
97
+ expect(screen.getByLabelText(/password/i)).toBeTruthy();
98
+ });
99
+
100
+ it('shows social buttons by default', () => {
101
+ render(<SignUp />);
102
+ expect(screen.getByText(/continue with google/i)).toBeTruthy();
103
+ });
104
+ });
105
+
106
+ describe('SocialButtons', () => {
107
+ beforeEach(() => vi.clearAllMocks());
108
+
109
+ it('renders both providers by default', () => {
110
+ render(<SocialButtons />);
111
+ expect(screen.getByText(/continue with google/i)).toBeTruthy();
112
+ expect(screen.getByText(/continue with github/i)).toBeTruthy();
113
+ });
114
+
115
+ it('renders only specified providers', () => {
116
+ render(<SocialButtons providers={['google']} />);
117
+ expect(screen.getByText(/continue with google/i)).toBeTruthy();
118
+ expect(screen.queryByText(/continue with github/i)).toBeNull();
119
+ });
120
+ });
121
+
122
+ describe('UserButton', () => {
123
+ beforeEach(() => vi.clearAllMocks());
124
+
125
+ it('renders avatar', () => {
126
+ render(<UserButton />);
127
+ expect(screen.getByRole('button', { name: /account menu/i })).toBeTruthy();
128
+ });
129
+
130
+ it('opens dropdown on click', async () => {
131
+ render(<UserButton />);
132
+ fireEvent.click(screen.getByRole('button', { name: /account menu/i }));
133
+ await waitFor(() => expect(screen.getByText(/sign out/i)).toBeTruthy());
134
+ });
135
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { KeyholeMark, GoogleIcon, GitHubIcon, CheckIcon } from '../src/icons';
5
+
6
+ describe('KeyholeMark', () => {
7
+ it('renders an SVG', () => {
8
+ const { container } = render(<KeyholeMark />);
9
+ expect(container.querySelector('svg')).toBeTruthy();
10
+ });
11
+ });
12
+
13
+ describe('GoogleIcon', () => {
14
+ it('renders an SVG', () => {
15
+ const { container } = render(<GoogleIcon />);
16
+ expect(container.querySelector('svg')).toBeTruthy();
17
+ });
18
+ });
19
+
20
+ describe('GitHubIcon', () => {
21
+ it('renders an SVG', () => {
22
+ const { container } = render(<GitHubIcon />);
23
+ expect(container.querySelector('svg')).toBeTruthy();
24
+ });
25
+ });
26
+
27
+ describe('CheckIcon', () => {
28
+ it('renders an SVG', () => {
29
+ const { container } = render(<CheckIcon />);
30
+ expect(container.querySelector('svg')).toBeTruthy();
31
+ });
32
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CSS, injectStyles } from '../src/styles';
3
+
4
+ describe('CSS', () => {
5
+ it('is a non-empty string', () => {
6
+ expect(typeof CSS).toBe('string');
7
+ expect(CSS.length).toBeGreaterThan(0);
8
+ });
9
+
10
+ it('contains .slyxup-root', () => {
11
+ expect(CSS).toContain('.slyxup-root');
12
+ });
13
+
14
+ it('contains --slx-accent', () => {
15
+ expect(CSS).toContain('--slx-accent');
16
+ });
17
+
18
+ it('contains dark mode media query', () => {
19
+ expect(CSS).toContain('prefers-color-scheme: dark');
20
+ });
21
+
22
+ it('contains @keyframes slx-shake', () => {
23
+ expect(CSS).toContain('@keyframes slx-shake');
24
+ });
25
+
26
+ it('contains @keyframes slx-spin', () => {
27
+ expect(CSS).toContain('@keyframes slx-spin');
28
+ });
29
+
30
+ it('contains prefers-reduced-motion', () => {
31
+ expect(CSS).toContain('prefers-reduced-motion');
32
+ });
33
+
34
+ it('contains card styles', () => {
35
+ expect(CSS).toContain('.slx-card');
36
+ });
37
+
38
+ it('contains button styles', () => {
39
+ expect(CSS).toContain('.slx-btn');
40
+ });
41
+ });
42
+
43
+ describe('injectStyles', () => {
44
+ it('is a function', () => {
45
+ expect(typeof injectStyles).toBe('function');
46
+ });
47
+ });
@@ -0,0 +1,2 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ export default defineConfig({ test: { globals: true, environment: 'happy-dom' } });