jazz-tools 0.18.5 → 0.18.7

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 (111) hide show
  1. package/.turbo/turbo-build.log +57 -57
  2. package/CHANGELOG.md +33 -0
  3. package/dist/better-auth/auth/client.d.ts.map +1 -1
  4. package/dist/better-auth/auth/client.js +7 -1
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/better-auth/auth/react.d.ts +0 -2145
  7. package/dist/better-auth/auth/react.d.ts.map +1 -1
  8. package/dist/better-auth/auth/react.js +2 -14
  9. package/dist/better-auth/auth/react.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +77 -22
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
  14. package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
  15. package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
  16. package/dist/chunk-CFAY3FMQ.js.map +1 -0
  17. package/dist/index.js +1 -1
  18. package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
  19. package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
  20. package/dist/inspector/index.d.ts +5 -1
  21. package/dist/inspector/index.d.ts.map +1 -1
  22. package/dist/inspector/index.js +318 -56
  23. package/dist/inspector/index.js.map +1 -1
  24. package/dist/inspector/register-custom-element.js +1 -1
  25. package/dist/inspector/ui/button.d.ts +1 -1
  26. package/dist/inspector/ui/button.d.ts.map +1 -1
  27. package/dist/inspector/ui/heading.d.ts +2 -1
  28. package/dist/inspector/ui/heading.d.ts.map +1 -1
  29. package/dist/inspector/ui/input.d.ts.map +1 -1
  30. package/dist/inspector/ui/modal.d.ts +16 -0
  31. package/dist/inspector/ui/modal.d.ts.map +1 -0
  32. package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
  33. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
  34. package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
  35. package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
  36. package/dist/inspector/viewer/new-app.d.ts +1 -4
  37. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  38. package/dist/react/hooks.d.ts +1 -1
  39. package/dist/react/hooks.d.ts.map +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -1
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react-core/hooks.d.ts +133 -0
  45. package/dist/react-core/hooks.d.ts.map +1 -1
  46. package/dist/react-core/index.js +83 -17
  47. package/dist/react-core/index.js.map +1 -1
  48. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
  49. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
  50. package/dist/react-native-core/hooks.d.ts +1 -1
  51. package/dist/react-native-core/hooks.d.ts.map +1 -1
  52. package/dist/react-native-core/index.js +3 -1
  53. package/dist/react-native-core/index.js.map +1 -1
  54. package/dist/testing.js +2 -2
  55. package/dist/testing.js.map +1 -1
  56. package/dist/tools/coValues/CoValueBase.d.ts +14 -0
  57. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  58. package/dist/tools/coValues/coMap.d.ts +0 -12
  59. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  60. package/dist/tools/coValues/inbox.d.ts +5 -5
  61. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  62. package/dist/tools/implementation/createContext.d.ts +2 -1
  63. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  64. package/dist/tools/tests/utils.d.ts.map +1 -1
  65. package/dist/worker/index.d.ts +12 -2
  66. package/dist/worker/index.d.ts.map +1 -1
  67. package/dist/worker/index.js +10 -4
  68. package/dist/worker/index.js.map +1 -1
  69. package/package.json +6 -4
  70. package/src/better-auth/auth/client.ts +8 -2
  71. package/src/better-auth/auth/react.tsx +2 -51
  72. package/src/better-auth/auth/server.ts +98 -24
  73. package/src/better-auth/auth/tests/client.test.ts +92 -4
  74. package/src/better-auth/auth/tests/react.test.tsx +43 -0
  75. package/src/better-auth/auth/tests/server.test.ts +276 -98
  76. package/src/inspector/custom-element.tsx +1 -1
  77. package/src/inspector/index.tsx +44 -0
  78. package/src/inspector/ui/button.tsx +15 -1
  79. package/src/inspector/ui/heading.tsx +7 -2
  80. package/src/inspector/ui/input.tsx +6 -2
  81. package/src/inspector/ui/modal.tsx +158 -0
  82. package/src/inspector/viewer/delete-local-data.tsx +101 -0
  83. package/src/inspector/viewer/new-app.tsx +3 -19
  84. package/src/react/hooks.tsx +1 -0
  85. package/src/react/index.ts +1 -0
  86. package/src/react-core/hooks.ts +162 -0
  87. package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
  88. package/src/react-native-core/hooks.tsx +1 -0
  89. package/src/tools/coValues/CoValueBase.ts +32 -0
  90. package/src/tools/coValues/coList.ts +35 -0
  91. package/src/tools/coValues/coMap.ts +0 -18
  92. package/src/tools/coValues/inbox.ts +190 -108
  93. package/src/tools/implementation/createContext.ts +9 -2
  94. package/src/tools/testing.ts +1 -1
  95. package/src/tools/tests/coFeed.test.ts +33 -22
  96. package/src/tools/tests/coList.test.ts +47 -4
  97. package/src/tools/tests/coMap.test.ts +13 -5
  98. package/src/tools/tests/coPlainText.test.ts +24 -0
  99. package/src/tools/tests/createContext.test.ts +24 -0
  100. package/src/tools/tests/deepLoading.test.ts +2 -0
  101. package/src/tools/tests/exportImport.test.ts +3 -1
  102. package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
  103. package/src/tools/tests/inbox.test.ts +293 -31
  104. package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
  105. package/src/tools/tests/utils.ts +1 -0
  106. package/src/worker/index.ts +21 -5
  107. package/tsup.config.ts +1 -1
  108. package/dist/chunk-3LE7N6TH.js.map +0 -1
  109. package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
  110. package/src/inspector/index.ts +0 -23
  111. /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -12,100 +12,44 @@ import {
12
12
  } from "vitest";
13
13
  import { OAuth2Server } from "oauth2-mock-server";
14
14
  import { jazzPlugin } from "../server.js";
15
- import { genericOAuth } from "better-auth/plugins";
16
-
17
- describe("Better Auth - Signup and Login Tests", async () => {
18
- const providerId = "test";
19
- const clientId = "test-client-id";
20
- const clientSecret = "test-client-secret";
21
- const server = new OAuth2Server();
22
- await server.start();
23
- const oauthPort = Number(server.issuer.url?.split(":")[2]!);
24
-
25
- let auth: ReturnType<
26
- typeof betterAuth<{
27
- plugins: ReturnType<typeof jazzPlugin | typeof genericOAuth>[];
28
- }>
29
- >;
30
- let accountCreationSpy: Mock;
31
- let verificationCreationSpy: Mock;
32
-
33
- beforeAll(async () => {
34
- await server.issuer.keys.generate("RS256");
35
-
36
- server.service.on("beforeUserinfo", (userInfoResponse) => {
37
- userInfoResponse.body = {
38
- email: "oauth2@test.com",
39
- name: "OAuth2 Test",
40
- sub: "oauth2",
41
- picture: "https://test.com/picture.png",
42
- email_verified: true,
43
- };
44
- userInfoResponse.statusCode = 200;
45
- });
46
- });
47
-
48
- afterAll(async () => {
49
- await server.stop();
50
- });
51
-
52
- beforeEach(() => {
53
- accountCreationSpy = vi.fn();
54
- verificationCreationSpy = vi.fn();
55
-
56
- // Create auth instance with in-memory database
57
- auth = betterAuth({
58
- database: memoryAdapter({
59
- user: [],
60
- session: [],
61
- verification: [],
62
- account: [],
63
- }),
64
- baseURL: "http://localhost:3000",
65
- plugins: [
66
- jazzPlugin(),
67
- genericOAuth({
68
- config: [
69
- {
70
- providerId,
71
- discoveryUrl: `http://localhost:${oauthPort}/.well-known/openid-configuration`,
72
- authorizationUrl: `http://localhost:${oauthPort}/authorize`,
73
- clientId: clientId,
74
- clientSecret: clientSecret,
75
- pkce: true,
76
- },
77
- ],
15
+ import { emailOTP, genericOAuth } from "better-auth/plugins";
16
+
17
+ describe("Better-Auth server plugin", async () => {
18
+ describe("Email & Password", () => {
19
+ let auth: ReturnType<
20
+ typeof betterAuth<{
21
+ plugins: ReturnType<typeof jazzPlugin>[];
22
+ }>
23
+ >;
24
+
25
+ let accountCreationSpy: Mock;
26
+
27
+ beforeEach(() => {
28
+ accountCreationSpy = vi.fn();
29
+
30
+ // Create auth instance with in-memory database
31
+ auth = betterAuth({
32
+ database: memoryAdapter({
33
+ user: [],
34
+ session: [],
35
+ verification: [],
36
+ account: [],
78
37
  }),
79
- ],
80
- emailAndPassword: {
81
- enabled: true,
82
- requireEmailVerification: false, // Disable for testing
83
- },
84
- socialProviders: {
85
- github: {
86
- clientId: "123",
87
- clientSecret: "123",
88
- },
89
- },
90
- databaseHooks: {
91
- user: {
92
- create: {
93
- after: accountCreationSpy,
94
- },
38
+ plugins: [jazzPlugin()],
39
+ emailAndPassword: {
40
+ enabled: true,
41
+ requireEmailVerification: false, // Disable for testing
95
42
  },
96
- verification: {
97
- create: {
98
- after: verificationCreationSpy,
43
+ databaseHooks: {
44
+ user: {
45
+ create: {
46
+ after: accountCreationSpy,
47
+ },
99
48
  },
100
49
  },
101
- },
102
- session: {
103
- expiresIn: 60 * 60 * 24 * 7, // 7 days
104
- },
50
+ });
105
51
  });
106
- });
107
52
 
108
- describe("User Registration (Signup)", () => {
109
53
  it("should successfully register a new user with email and password", async () => {
110
54
  const userData = {
111
55
  name: "test",
@@ -203,9 +147,7 @@ describe("Better Auth - Signup and Login Tests", async () => {
203
147
  expect.any(Object),
204
148
  );
205
149
  });
206
- });
207
150
 
208
- describe("User login (Signin)", () => {
209
151
  it("should successfully login a new user with email and password", async () => {
210
152
  const userData = {
211
153
  name: "test",
@@ -250,7 +192,90 @@ describe("Better Auth - Signup and Login Tests", async () => {
250
192
  });
251
193
  });
252
194
 
253
- describe("Social Login", () => {
195
+ describe("OAuth/Social plugin", async () => {
196
+ const providerId = "test";
197
+ const clientId = "test-client-id";
198
+ const clientSecret = "test-client-secret";
199
+ const server = new OAuth2Server();
200
+ await server.start();
201
+ const oauthPort = Number(server.issuer.url?.split(":")[2]!);
202
+
203
+ let auth: ReturnType<
204
+ typeof betterAuth<{
205
+ plugins: ReturnType<typeof jazzPlugin | typeof genericOAuth>[];
206
+ }>
207
+ >;
208
+ let accountCreationSpy: Mock;
209
+ let verificationCreationSpy: Mock;
210
+
211
+ beforeAll(async () => {
212
+ await server.issuer.keys.generate("RS256");
213
+
214
+ server.service.on("beforeUserinfo", (userInfoResponse) => {
215
+ userInfoResponse.body = {
216
+ email: "oauth2@test.com",
217
+ name: "OAuth2 Test",
218
+ sub: "oauth2",
219
+ picture: "https://test.com/picture.png",
220
+ email_verified: true,
221
+ };
222
+ userInfoResponse.statusCode = 200;
223
+ });
224
+ });
225
+
226
+ afterAll(async () => {
227
+ await server.stop();
228
+ });
229
+
230
+ beforeEach(() => {
231
+ accountCreationSpy = vi.fn();
232
+ verificationCreationSpy = vi.fn();
233
+
234
+ // Create auth instance with in-memory database
235
+ auth = betterAuth({
236
+ database: memoryAdapter({
237
+ user: [],
238
+ session: [],
239
+ verification: [],
240
+ account: [],
241
+ }),
242
+ baseURL: "http://localhost:3000",
243
+ plugins: [
244
+ jazzPlugin(),
245
+ genericOAuth({
246
+ config: [
247
+ {
248
+ providerId,
249
+ discoveryUrl: `http://localhost:${oauthPort}/.well-known/openid-configuration`,
250
+ authorizationUrl: `http://localhost:${oauthPort}/authorize`,
251
+ clientId: clientId,
252
+ clientSecret: clientSecret,
253
+ pkce: true,
254
+ },
255
+ ],
256
+ }),
257
+ ],
258
+ socialProviders: {
259
+ github: {
260
+ clientId: "123",
261
+ clientSecret: "123",
262
+ },
263
+ },
264
+ databaseHooks: {
265
+ user: {
266
+ create: {
267
+ after: accountCreationSpy,
268
+ },
269
+ },
270
+ verification: {
271
+ create: {
272
+ after: verificationCreationSpy,
273
+ },
274
+ },
275
+ },
276
+ });
277
+ });
278
+
254
279
  it("should store jazzAuth in verification table when using social provider", async () => {
255
280
  await auth.api.signInSocial({
256
281
  body: {
@@ -266,13 +291,11 @@ describe("Better Auth - Signup and Login Tests", async () => {
266
291
  },
267
292
  });
268
293
 
269
- expect(verificationCreationSpy).toHaveBeenCalledTimes(1);
270
- expect(verificationCreationSpy).toHaveBeenCalledWith(
271
- expect.objectContaining({
272
- value: expect.stringContaining('"accountID":"123"'),
273
- }),
274
- expect.any(Object),
275
- );
294
+ expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
295
+ expect(verificationCreationSpy.mock.calls[1]?.[0]).toMatchObject({
296
+ identifier: expect.stringMatching("jazz-auth-"),
297
+ value: expect.stringContaining('"accountID":"123"'),
298
+ });
276
299
  });
277
300
 
278
301
  it("should create a new account with jazz auth when using social provider", async () => {
@@ -311,4 +334,159 @@ describe("Better Auth - Signup and Login Tests", async () => {
311
334
  );
312
335
  });
313
336
  });
337
+
338
+ describe("Email OTP plugin", () => {
339
+ let auth: ReturnType<
340
+ typeof betterAuth<{
341
+ plugins: ReturnType<typeof jazzPlugin | typeof emailOTP>[];
342
+ }>
343
+ >;
344
+
345
+ let accountCreationSpy: Mock;
346
+ let verificationCreationSpy: Mock;
347
+ let sendVerificationOTPSpy: Mock;
348
+
349
+ beforeEach(() => {
350
+ accountCreationSpy = vi.fn();
351
+ verificationCreationSpy = vi.fn();
352
+ sendVerificationOTPSpy = vi.fn();
353
+ // Create auth instance with in-memory database
354
+ auth = betterAuth({
355
+ database: memoryAdapter({
356
+ user: [],
357
+ session: [],
358
+ verification: [],
359
+ account: [],
360
+ }),
361
+ plugins: [
362
+ jazzPlugin(),
363
+ emailOTP({
364
+ allowedAttempts: 5,
365
+ otpLength: 6,
366
+ expiresIn: 600,
367
+ sendVerificationOTP: sendVerificationOTPSpy,
368
+ }),
369
+ ],
370
+ emailAndPassword: {
371
+ enabled: true,
372
+ requireEmailVerification: false, // Disable for testing
373
+ },
374
+ databaseHooks: {
375
+ user: {
376
+ create: {
377
+ after: accountCreationSpy,
378
+ },
379
+ },
380
+ verification: {
381
+ create: {
382
+ after: verificationCreationSpy,
383
+ },
384
+ },
385
+ },
386
+ });
387
+ });
388
+
389
+ it("should create a new account with jazz auth when using email OTP", async () => {
390
+ let OTP: string = "";
391
+
392
+ sendVerificationOTPSpy.mockImplementationOnce(({ otp }) => {
393
+ OTP = otp;
394
+ });
395
+
396
+ await auth.api.sendVerificationOTP({
397
+ headers: {
398
+ "x-jazz-auth": JSON.stringify({
399
+ accountID: "123",
400
+ secretSeed: [1, 2, 3],
401
+ accountSecret: "123",
402
+ }),
403
+ },
404
+ body: {
405
+ email: "email@email.it",
406
+ type: "sign-in",
407
+ },
408
+ });
409
+
410
+ expect(accountCreationSpy).toHaveBeenCalledTimes(0);
411
+ expect(sendVerificationOTPSpy).toHaveBeenCalledTimes(1);
412
+ expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
413
+ expect(verificationCreationSpy.mock.calls[0]?.[0]).toMatchObject(
414
+ expect.objectContaining({
415
+ identifier: "jazz-auth-sign-in-otp-email@email.it",
416
+ value: expect.stringContaining('"accountID":"123"'),
417
+ }),
418
+ );
419
+
420
+ await auth.api.signInEmailOTP({
421
+ body: {
422
+ email: "email@email.it",
423
+ otp: OTP,
424
+ },
425
+ });
426
+
427
+ expect(accountCreationSpy).toHaveBeenCalledTimes(1);
428
+ expect(accountCreationSpy).toHaveBeenCalledWith(
429
+ expect.objectContaining({ accountID: "123" }),
430
+ expect.any(Object),
431
+ );
432
+ });
433
+
434
+ it("should not expect Jazz's credentials using Email OTP for sign-in an already registered user", async () => {
435
+ // 1. User registration
436
+ const userData = {
437
+ name: "test",
438
+ email: "test@example.com",
439
+ password: "securePassword123",
440
+ };
441
+
442
+ const jazzAuth = {
443
+ accountID: "123",
444
+ secretSeed: [1, 2, 3],
445
+ accountSecret: "123",
446
+ provider: "better-auth",
447
+ };
448
+
449
+ await auth.api.signUpEmail({
450
+ body: userData,
451
+ headers: {
452
+ "x-jazz-auth": JSON.stringify(jazzAuth),
453
+ },
454
+ });
455
+
456
+ expect(accountCreationSpy).toHaveBeenCalledTimes(1);
457
+
458
+ // 2. Try to sign-in with OTP
459
+ let OTP: string = "";
460
+
461
+ sendVerificationOTPSpy.mockImplementationOnce(({ otp }) => {
462
+ OTP = otp;
463
+ });
464
+
465
+ await auth.api.sendVerificationOTP({
466
+ body: {
467
+ email: "test@example.com",
468
+ type: "sign-in",
469
+ },
470
+ });
471
+
472
+ expect(sendVerificationOTPSpy).toHaveBeenCalledTimes(1);
473
+ expect(verificationCreationSpy).toHaveBeenCalledTimes(1);
474
+
475
+ const result = await auth.api.signInEmailOTP({
476
+ body: {
477
+ email: "test@example.com",
478
+ otp: OTP,
479
+ },
480
+ });
481
+
482
+ expect(accountCreationSpy).toHaveBeenCalledTimes(1); // still only 1
483
+ expect(result).toMatchObject({
484
+ user: {
485
+ id: expect.any(String),
486
+ email: "test@example.com",
487
+ name: "test",
488
+ },
489
+ });
490
+ });
491
+ });
314
492
  });
@@ -1,6 +1,6 @@
1
1
  import { Account } from "jazz-tools";
2
2
  import { createRoot } from "react-dom/client";
3
- import { JazzInspectorInternal } from "./index.js";
3
+ import { JazzInspectorInternal } from "./viewer/new-app.js";
4
4
 
5
5
  export class JazzInspectorElement extends HTMLElement {
6
6
  private root: ReturnType<typeof createRoot> | null = null;
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+
3
+ export { JazzInspectorInternal } from "./viewer/new-app.js";
4
+ export { PageStack } from "./viewer/page-stack.js";
5
+ export { Breadcrumbs } from "./viewer/breadcrumbs.js";
6
+ export { AccountOrGroupText } from "./viewer/account-or-group-text.js";
7
+
8
+ export { Button } from "./ui/button.js";
9
+ export { Input } from "./ui/input.js";
10
+ export { Select } from "./ui/select.js";
11
+ export { Icon } from "./ui/icon.js";
12
+ export { GlobalStyles } from "./ui/global-styles.js";
13
+
14
+ export {
15
+ resolveCoValue,
16
+ useResolvedCoValue,
17
+ } from "./viewer/use-resolve-covalue.js";
18
+
19
+ export type { PageInfo } from "./viewer/types.js";
20
+
21
+ import { setup } from "goober";
22
+ import { useJazzContext } from "jazz-tools/react-core";
23
+ import { Account } from "jazz-tools";
24
+
25
+ import { JazzInspectorInternal } from "./viewer/new-app.js";
26
+ import { Position } from "./viewer/inspector-button.js";
27
+
28
+ export function JazzInspector({ position = "right" }: { position?: Position }) {
29
+ const context = useJazzContext<Account>();
30
+ const localNode = context.node;
31
+ const me = "me" in context ? context.me : undefined;
32
+
33
+ if (process.env.NODE_ENV !== "development") return null;
34
+
35
+ return (
36
+ <JazzInspectorInternal
37
+ position={position}
38
+ localNode={localNode}
39
+ accountId={me?.$jazz.raw.id}
40
+ />
41
+ );
42
+ }
43
+
44
+ setup(React.createElement);
@@ -2,7 +2,7 @@ import { styled } from "goober";
2
2
  import { forwardRef } from "react";
3
3
 
4
4
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
- variant?: "primary" | "secondary" | "link" | "plain";
5
+ variant?: "primary" | "secondary" | "link" | "plain" | "destructive";
6
6
  children?: React.ReactNode;
7
7
  className?: string;
8
8
  disabled?: boolean;
@@ -29,6 +29,9 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
29
29
  border-color: var(--j-primary-color);
30
30
  color: white;
31
31
  font-weight: 500;
32
+ &:hover {
33
+ opacity: 0.8;
34
+ }
32
35
  `;
33
36
  case "secondary":
34
37
  return `
@@ -47,6 +50,17 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
47
50
  text-decoration: underline;
48
51
  }
49
52
  `;
53
+ case "destructive":
54
+ return `
55
+ padding: 0.375rem 0.75rem;
56
+ background-color: var(--j-destructive-color);
57
+ border-color: var(--j-destructive-color);
58
+ color: white;
59
+ font-weight: 500;
60
+ &:hover {
61
+ opacity: 0.8;
62
+ }
63
+ `;
50
64
  default:
51
65
  return "";
52
66
  }
@@ -10,6 +10,11 @@ const StyledHeading = styled("h1")<{ className?: string }>`
10
10
  export function Heading({
11
11
  children,
12
12
  className,
13
- }: React.PropsWithChildren<{ className?: string }>) {
14
- return <StyledHeading className={className}>{children}</StyledHeading>;
13
+ id,
14
+ }: React.PropsWithChildren<{ className?: string; id?: string }>) {
15
+ return (
16
+ <StyledHeading className={className} id={id}>
17
+ {children}
18
+ </StyledHeading>
19
+ );
15
20
  }
@@ -27,7 +27,7 @@ const StyledInput = styled("input")`
27
27
  box-shadow: var(--j-shadow-sm);
28
28
  font-weight: 500;
29
29
  background-color: white;
30
- color: var(--text-color-strong);
30
+ color: var(--j-text-color-strong);
31
31
 
32
32
  @media (prefers-color-scheme: dark) {
33
33
  background-color: var(--j-foreground);
@@ -41,7 +41,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
41
41
 
42
42
  return (
43
43
  <Container className={className}>
44
- <label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
44
+ <label
45
+ htmlFor={id}
46
+ className={hideLabel ? "j-sr-only" : ""}
47
+ style={{ color: "var(--j-text-color)" }}
48
+ >
45
49
  {label}
46
50
  </label>
47
51
  <StyledInput ref={ref} {...inputProps} id={id} />
@@ -0,0 +1,158 @@
1
+ import { styled } from "goober";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
+ import { Button } from "./button.js";
4
+ import { Heading } from "./heading.js";
5
+
6
+ interface ModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ heading: string;
10
+ text?: string;
11
+ children?: React.ReactNode;
12
+ confirmText?: string;
13
+ cancelText?: string;
14
+ onConfirm?: () => void;
15
+ onCancel?: () => void;
16
+ showButtons?: boolean;
17
+ className?: string;
18
+ }
19
+
20
+ const ModalContent = styled("dialog")`
21
+ background-color: var(--j-background);
22
+ border-radius: var(--j-radius-lg);
23
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
24
+ border: 1px solid var(--j-border-color);
25
+ max-width: 32rem;
26
+ margin-block: auto;
27
+ margin-inline: auto;
28
+ &::backdrop {
29
+ background-color: rgba(0, 0, 0, 0.7);
30
+ }
31
+
32
+ `;
33
+
34
+ const ModalHeader = styled("div")`
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: flex-start;
38
+ padding: 1.5rem 1.5rem 0 1.5rem;
39
+ gap: 1rem;
40
+ `;
41
+
42
+ const ModalBody = styled("div")`
43
+ padding: 1rem 1.5rem;
44
+ flex: 1;
45
+ `;
46
+
47
+ const ModalFooter = styled("div")`
48
+ display: flex;
49
+ justify-content: flex-end;
50
+ gap: 0.75rem;
51
+ padding: 0 1.5rem 1.5rem 1.5rem;
52
+ `;
53
+
54
+ const CloseButton = styled("button")`
55
+ background: none;
56
+ border: none;
57
+ cursor: pointer;
58
+ padding: 0.25rem;
59
+ border-radius: var(--j-radius-sm);
60
+ color: var(--j-text-color);
61
+ font-size: 1.25rem;
62
+ line-height: 1;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ min-width: 2rem;
67
+ min-height: 2rem;
68
+
69
+ &:hover {
70
+ background-color: var(--j-foreground);
71
+ }
72
+
73
+ &:focus-visible {
74
+ outline: 2px solid var(--j-border-focus);
75
+ outline-offset: 2px;
76
+ }
77
+ `;
78
+
79
+ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
80
+ (
81
+ {
82
+ isOpen,
83
+ onClose,
84
+ heading,
85
+ text,
86
+ children,
87
+ confirmText = "Confirm",
88
+ cancelText = "Cancel",
89
+ onConfirm,
90
+ onCancel,
91
+ showButtons = true,
92
+ className,
93
+ },
94
+ ref,
95
+ ) => {
96
+ const modalRef = useRef<HTMLDialogElement>(null);
97
+
98
+ useEffect(() => {
99
+ if (isOpen) {
100
+ modalRef.current?.showModal();
101
+ } else {
102
+ onClose();
103
+ modalRef.current?.close();
104
+ }
105
+ }, [isOpen, onClose]);
106
+
107
+ const handleConfirm = () => {
108
+ onConfirm?.();
109
+ onClose();
110
+ };
111
+
112
+ const handleCancel = () => {
113
+ onCancel?.();
114
+ onClose();
115
+ };
116
+
117
+ if (!isOpen) return null;
118
+
119
+ return (
120
+ <ModalContent
121
+ ref={ref || modalRef}
122
+ className={className}
123
+ role="dialog"
124
+ aria-labelledby="modal-heading"
125
+ onClose={onClose}
126
+ >
127
+ <ModalHeader>
128
+ <Heading id="modal-heading">{heading}</Heading>
129
+ <CloseButton onClick={onClose} aria-label="Close modal" type="button">
130
+ ×
131
+ </CloseButton>
132
+ </ModalHeader>
133
+
134
+ <ModalBody>
135
+ {text && (
136
+ <p style={{ margin: "0 0 1rem 0", color: "var(--j-text-color)" }}>
137
+ {text}
138
+ </p>
139
+ )}
140
+ {children}
141
+ </ModalBody>
142
+
143
+ {showButtons && (
144
+ <ModalFooter>
145
+ <Button variant="secondary" onClick={handleCancel}>
146
+ {cancelText}
147
+ </Button>
148
+ <Button variant="primary" onClick={handleConfirm}>
149
+ {confirmText}
150
+ </Button>
151
+ </ModalFooter>
152
+ )}
153
+ </ModalContent>
154
+ );
155
+ },
156
+ );
157
+
158
+ Modal.displayName = "Modal";