torch-glare 2.4.1 → 2.4.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 (79) hide show
  1. package/apps/lib/components/Button.tsx +1 -1
  2. package/apps/lib/components/Card.tsx +47 -20
  3. package/apps/lib/components/ColorPicker.tsx +441 -0
  4. package/apps/lib/components/ConclusionHeader.tsx +148 -0
  5. package/apps/lib/components/DatePicker.tsx +2 -0
  6. package/apps/lib/components/Drawer.tsx +66 -24
  7. package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
  8. package/apps/lib/components/FormBuilder/context.ts +71 -0
  9. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
  10. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
  11. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
  12. package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
  13. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
  14. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
  15. package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
  16. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
  17. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
  18. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
  19. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
  20. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
  21. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
  22. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
  23. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
  24. package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
  25. package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
  26. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
  27. package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
  28. package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
  29. package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
  30. package/apps/lib/components/FormBuilder/header.tsx +105 -0
  31. package/apps/lib/components/FormBuilder/index.ts +37 -0
  32. package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
  33. package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
  34. package/apps/lib/components/FormBuilder/submit.tsx +41 -0
  35. package/apps/lib/components/FormBuilder/types.ts +264 -0
  36. package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
  37. package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
  38. package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
  39. package/apps/lib/components/FormRenderer/index.ts +9 -0
  40. package/apps/lib/components/FormRenderer/types.ts +79 -0
  41. package/apps/lib/components/FormSummary.tsx +282 -0
  42. package/apps/lib/components/ImageAttachment.tsx +36 -61
  43. package/apps/lib/components/Label.tsx +49 -42
  44. package/apps/lib/components/RadioCard.tsx +2 -0
  45. package/apps/lib/components/SearchableSelect.tsx +9 -3
  46. package/apps/lib/components/SectionBlock.tsx +16 -8
  47. package/apps/lib/components/Select.tsx +41 -120
  48. package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
  49. package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
  50. package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
  51. package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
  52. package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
  53. package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
  54. package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
  55. package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
  56. package/apps/lib/components/TextEditor/index.ts +7 -0
  57. package/apps/lib/components/Textarea.tsx +1 -1
  58. package/apps/lib/registry.json +51 -58
  59. package/apps/lib/tsconfig.tsbuildinfo +1 -0
  60. package/apps/lib/utils/color.ts +175 -0
  61. package/docs/components/card.md +4 -2
  62. package/docs/components/chart-block-tool.md +5 -4
  63. package/docs/components/color-picker.md +101 -0
  64. package/docs/components/conclusion-header.md +80 -0
  65. package/docs/components/drawer.md +153 -102
  66. package/docs/components/form-builder.md +270 -0
  67. package/docs/components/form-renderer.md +228 -0
  68. package/docs/components/form-summary.md +123 -0
  69. package/docs/components/image-attachment.md +10 -4
  70. package/docs/components/table-dnd-wrapper.md +5 -3
  71. package/docs/components/text-editor.md +18 -0
  72. package/docs/how-to/form-and-list-recipes.md +26 -19
  73. package/docs/how-to/forms-with-form-builder.md +408 -0
  74. package/docs/how-to/guides.md +153 -179
  75. package/docs/tutorials/building-first-form.md +150 -159
  76. package/package.json +1 -1
  77. /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
  78. /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
  79. /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
@@ -13,6 +13,12 @@ related:
13
13
 
14
14
  Practical, step-by-step guides for accomplishing common tasks with TORCH Glare.
15
15
 
16
+ > **Building a form? Use [`FormBuilder`](../components/form-builder.md).** The Form Validation
17
+ > section below shows the low-level, hand-wired resolver plumbing for understanding — but in an
18
+ > app you author fields as `FormBuilder` JSX children and pass a react-hook-form resolver, rather
19
+ > than tracking field state yourself. See the [Forms with FormBuilder](./forms-with-form-builder.md)
20
+ > guide.
21
+
16
22
  ## Table of Contents
17
23
 
18
24
  1. [Custom Themes](#custom-themes)
@@ -37,18 +43,18 @@ export default {
37
43
  theme: {
38
44
  extend: {
39
45
  colors: {
40
- 'brand-primary': '#4200FF',
41
- 'brand-secondary': '#5215CC',
42
- 'brand-accent': '#CFBEFF',
43
- 'brand-dark': '#0D0F4E',
44
- 'brand-light': '#F9F9F9',
46
+ "brand-primary": "#4200FF",
47
+ "brand-secondary": "#5215CC",
48
+ "brand-accent": "#CFBEFF",
49
+ "brand-dark": "#0D0F4E",
50
+ "brand-light": "#F9F9F9",
45
51
  },
46
52
  },
47
53
  },
48
54
  plugins: [
49
- require('mapping-color-system'),
50
- require('glare-torch-mode'),
51
- require('glare-typography'),
55
+ require("mapping-color-system"),
56
+ require("glare-torch-mode"),
57
+ require("glare-typography"),
52
58
  ],
53
59
  } satisfies Config;
54
60
  ```
@@ -63,15 +69,15 @@ export default {
63
69
 
64
70
  @layer base {
65
71
  [data-theme="light"] {
66
- --background-presentation-action-primary: #4200FF;
67
- --content-presentation-action-light-primary: #FFFFFF;
68
- --border-presentation-action-primary: #5215CC;
72
+ --background-presentation-action-primary: #4200ff;
73
+ --content-presentation-action-light-primary: #ffffff;
74
+ --border-presentation-action-primary: #5215cc;
69
75
  }
70
76
 
71
77
  [data-theme="dark"] {
72
- --background-presentation-action-primary: #5215CC;
73
- --content-presentation-action-light-primary: #F9F9F9;
74
- --border-presentation-action-primary: #CFBEFF;
78
+ --background-presentation-action-primary: #5215cc;
79
+ --content-presentation-action-light-primary: #f9f9f9;
80
+ --border-presentation-action-primary: #cfbeff;
75
81
  }
76
82
  }
77
83
  ```
@@ -88,9 +94,9 @@ export function BrandButton({ children, ...props }: any) {
88
94
  <Button
89
95
  {...props}
90
96
  className={cn(
91
- 'bg-brand-primary hover:bg-brand-secondary',
92
- 'text-white border-brand-accent',
93
- props.className
97
+ "bg-brand-primary hover:bg-brand-secondary",
98
+ "text-white border-brand-accent",
99
+ props.className,
94
100
  )}
95
101
  >
96
102
  {children}
@@ -104,21 +110,21 @@ export function BrandButton({ children, ...props }: any) {
104
110
  ```tsx
105
111
  // hooks/useCustomTheme.ts
106
112
  import { useTheme } from "@/providers/ThemeProvider";
107
- import { useEffect } from 'react';
113
+ import { useEffect } from "react";
108
114
 
109
115
  export function useCustomTheme() {
110
116
  const { theme, updateTheme } = useTheme();
111
117
 
112
118
  useEffect(() => {
113
119
  // Apply custom theme classes
114
- if (theme === 'light') {
115
- document.documentElement.classList.add('brand-light-theme');
120
+ if (theme === "light") {
121
+ document.documentElement.classList.add("brand-light-theme");
116
122
  } else {
117
- document.documentElement.classList.add('brand-dark-theme');
123
+ document.documentElement.classList.add("brand-dark-theme");
118
124
  }
119
125
 
120
126
  return () => {
121
- document.documentElement.classList.remove('brand-light-theme', 'brand-dark-theme');
127
+ document.documentElement.classList.remove("brand-light-theme", "brand-dark-theme");
122
128
  };
123
129
  }, [theme]);
124
130
 
@@ -135,9 +141,7 @@ import { ThemeProvider } from "@/providers/ThemeProvider";
135
141
  export function BrandThemeProvider({ children }: { children: React.ReactNode }) {
136
142
  return (
137
143
  <ThemeProvider defaultTheme="light" defaultThemeMode="TORCH">
138
- <div className="brand-theme-wrapper">
139
- {children}
140
- </div>
144
+ <div className="brand-theme-wrapper">{children}</div>
141
145
  </ThemeProvider>
142
146
  );
143
147
  }
@@ -147,20 +151,20 @@ export function BrandThemeProvider({ children }: { children: React.ReactNode })
147
151
 
148
152
  ```tsx
149
153
  // __tests__/BrandButton.test.tsx
150
- import { render } from '@testing-library/react';
154
+ import { render } from "@testing-library/react";
151
155
  import { ThemeProvider } from "@/providers/ThemeProvider";
152
- import { BrandButton } from '../components/BrandButton';
156
+ import { BrandButton } from "../components/BrandButton";
153
157
 
154
- describe('BrandButton', () => {
155
- it('applies custom brand colors', () => {
158
+ describe("BrandButton", () => {
159
+ it("applies custom brand colors", () => {
156
160
  const { container } = render(
157
161
  <ThemeProvider defaultTheme="light">
158
162
  <BrandButton>Click me</BrandButton>
159
- </ThemeProvider>
163
+ </ThemeProvider>,
160
164
  );
161
165
 
162
- const button = container.querySelector('button');
163
- expect(button).toHaveClass('bg-brand-primary');
166
+ const button = container.querySelector("button");
167
+ expect(button).toHaveClass("bg-brand-primary");
164
168
  });
165
169
  });
166
170
  ```
@@ -175,7 +179,7 @@ Implement comprehensive form validation with error handling.
175
179
 
176
180
  ```tsx
177
181
  // hooks/useFormValidation.ts
178
- import { useState, useCallback } from 'react';
182
+ import { useState, useCallback } from "react";
179
183
 
180
184
  type ValidationRules<T> = {
181
185
  [K in keyof T]?: Array<(value: T[K]) => string | undefined>;
@@ -183,7 +187,7 @@ type ValidationRules<T> = {
183
187
 
184
188
  export function useFormValidation<T extends Record<string, any>>(
185
189
  initialValues: T,
186
- rules: ValidationRules<T>
190
+ rules: ValidationRules<T>,
187
191
  ) {
188
192
  const [values, setValues] = useState<T>(initialValues);
189
193
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
@@ -200,7 +204,7 @@ export function useFormValidation<T extends Record<string, any>>(
200
204
  }
201
205
  return undefined;
202
206
  },
203
- [rules]
207
+ [rules],
204
208
  );
205
209
 
206
210
  const handleChange = useCallback(
@@ -213,7 +217,7 @@ export function useFormValidation<T extends Record<string, any>>(
213
217
  setErrors((prev) => ({ ...prev, [field]: error }));
214
218
  }
215
219
  },
216
- [touched, validateField]
220
+ [touched, validateField],
217
221
  );
218
222
 
219
223
  const handleBlur = useCallback(
@@ -222,7 +226,7 @@ export function useFormValidation<T extends Record<string, any>>(
222
226
  const error = validateField(field, values[field]);
223
227
  setErrors((prev) => ({ ...prev, [field]: error }));
224
228
  },
225
- [values, validateField]
229
+ [values, validateField],
226
230
  );
227
231
 
228
232
  const validateForm = useCallback((): boolean => {
@@ -238,9 +242,7 @@ export function useFormValidation<T extends Record<string, any>>(
238
242
  });
239
243
 
240
244
  setErrors(newErrors);
241
- setTouched(
242
- Object.keys(rules).reduce((acc, field) => ({ ...acc, [field]: true }), {})
243
- );
245
+ setTouched(Object.keys(rules).reduce((acc, field) => ({ ...acc, [field]: true }), {}));
244
246
 
245
247
  return isValid;
246
248
  }, [rules, values, validateField]);
@@ -267,16 +269,16 @@ export function useFormValidation<T extends Record<string, any>>(
267
269
 
268
270
  ```tsx
269
271
  // utils/validationRules.ts
270
- export const required = (message = 'This field is required') => {
272
+ export const required = (message = "This field is required") => {
271
273
  return (value: any) => {
272
- if (!value || (typeof value === 'string' && !value.trim())) {
274
+ if (!value || (typeof value === "string" && !value.trim())) {
273
275
  return message;
274
276
  }
275
277
  return undefined;
276
278
  };
277
279
  };
278
280
 
279
- export const email = (message = 'Invalid email address') => {
281
+ export const email = (message = "Invalid email address") => {
280
282
  return (value: string) => {
281
283
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
282
284
  if (value && !emailRegex.test(value)) {
@@ -327,54 +329,47 @@ export const matchField = (otherField: string, message?: string) => {
327
329
 
328
330
  ```tsx
329
331
  // components/RegistrationForm.tsx
330
- 'use client';
332
+ "use client";
331
333
 
332
- import { useFormValidation } from '../hooks/useFormValidation';
333
- import { required, email, minLength } from '../utils/validationRules';
334
+ import { useFormValidation } from "../hooks/useFormValidation";
335
+ import { required, email, minLength } from "../utils/validationRules";
334
336
  import { Button } from "@/components/Button";
335
337
  import { LabelField } from "@/components/LabelField";
336
338
  import { toast } from "@/components/Toast";
337
339
 
338
340
  export default function RegistrationForm() {
339
- const {
340
- values,
341
- errors,
342
- touched,
343
- handleChange,
344
- handleBlur,
345
- validateForm,
346
- reset,
347
- } = useFormValidation(
348
- {
349
- email: '',
350
- password: '',
351
- confirmPassword: '',
352
- },
353
- {
354
- email: [required(), email()],
355
- password: [required(), minLength(8, 'Password must be at least 8 characters')],
356
- confirmPassword: [
357
- required(),
358
- (value) => {
359
- if (value !== values.password) {
360
- return 'Passwords must match';
361
- }
362
- return undefined;
363
- },
364
- ],
365
- }
366
- );
341
+ const { values, errors, touched, handleChange, handleBlur, validateForm, reset } =
342
+ useFormValidation(
343
+ {
344
+ email: "",
345
+ password: "",
346
+ confirmPassword: "",
347
+ },
348
+ {
349
+ email: [required(), email()],
350
+ password: [required(), minLength(8, "Password must be at least 8 characters")],
351
+ confirmPassword: [
352
+ required(),
353
+ (value) => {
354
+ if (value !== values.password) {
355
+ return "Passwords must match";
356
+ }
357
+ return undefined;
358
+ },
359
+ ],
360
+ },
361
+ );
367
362
 
368
363
  const handleSubmit = async (e: React.FormEvent) => {
369
364
  e.preventDefault();
370
365
 
371
366
  if (!validateForm()) {
372
- toast.error('Please fix validation errors');
367
+ toast.error("Please fix validation errors");
373
368
  return;
374
369
  }
375
370
 
376
371
  // Submit form
377
- toast.success('Registration successful!');
372
+ toast.success("Registration successful!");
378
373
  reset();
379
374
  };
380
375
 
@@ -386,8 +381,8 @@ export default function RegistrationForm() {
386
381
  requiredLabel="*"
387
382
  type="email"
388
383
  value={values.email}
389
- onChange={handleChange('email')}
390
- onBlur={handleBlur('email')}
384
+ onChange={handleChange("email")}
385
+ onBlur={handleBlur("email")}
391
386
  errorMessage={touched.email ? errors.email : undefined}
392
387
  />
393
388
 
@@ -397,8 +392,8 @@ export default function RegistrationForm() {
397
392
  requiredLabel="*"
398
393
  type="password"
399
394
  value={values.password}
400
- onChange={handleChange('password')}
401
- onBlur={handleBlur('password')}
395
+ onChange={handleChange("password")}
396
+ onBlur={handleBlur("password")}
402
397
  errorMessage={touched.password ? errors.password : undefined}
403
398
  />
404
399
 
@@ -408,8 +403,8 @@ export default function RegistrationForm() {
408
403
  requiredLabel="*"
409
404
  type="password"
410
405
  value={values.confirmPassword}
411
- onChange={handleChange('confirmPassword')}
412
- onBlur={handleBlur('confirmPassword')}
406
+ onChange={handleChange("confirmPassword")}
407
+ onBlur={handleBlur("confirmPassword")}
413
408
  errorMessage={touched.confirmPassword ? errors.confirmPassword : undefined}
414
409
  />
415
410
 
@@ -425,7 +420,7 @@ export default function RegistrationForm() {
425
420
 
426
421
  ```tsx
427
422
  // Example: Check if email is available
428
- export const emailAvailable = (message = 'Email already taken') => {
423
+ export const emailAvailable = (message = "Email already taken") => {
429
424
  return async (value: string) => {
430
425
  if (!value) return undefined;
431
426
 
@@ -437,7 +432,7 @@ export const emailAvailable = (message = 'Email already taken') => {
437
432
  return message;
438
433
  }
439
434
  } catch (error) {
440
- console.error('Email validation error:', error);
435
+ console.error("Email validation error:", error);
441
436
  }
442
437
 
443
438
  return undefined;
@@ -455,11 +450,11 @@ Implement comprehensive dark mode support.
455
450
 
456
451
  ```tsx
457
452
  // components/DarkModeToggle.tsx
458
- 'use client';
453
+ "use client";
459
454
 
460
455
  import { Button } from "@/components/Button";
461
456
  import { useTheme } from "@/providers/ThemeProvider";
462
- import { useEffect, useState } from 'react';
457
+ import { useEffect, useState } from "react";
463
458
 
464
459
  export default function DarkModeToggle() {
465
460
  const { theme, updateTheme } = useTheme();
@@ -483,10 +478,10 @@ export default function DarkModeToggle() {
483
478
  theme={theme as any}
484
479
  variant="PrimeContStyle"
485
480
  buttonType="icon"
486
- onClick={() => updateTheme(theme === 'light' ? 'dark' : 'light')}
487
- aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
481
+ onClick={() => updateTheme(theme === "light" ? "dark" : "light")}
482
+ aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
488
483
  >
489
- {theme === 'light' ? (
484
+ {theme === "light" ? (
490
485
  <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
491
486
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
492
487
  </svg>
@@ -508,7 +503,7 @@ export default function DarkModeToggle() {
508
503
 
509
504
  ```tsx
510
505
  // hooks/useSystemTheme.ts
511
- import { useEffect } from 'react';
506
+ import { useEffect } from "react";
512
507
  import { useTheme } from "@/providers/ThemeProvider";
513
508
 
514
509
  export function useSystemTheme() {
@@ -516,21 +511,21 @@ export function useSystemTheme() {
516
511
 
517
512
  useEffect(() => {
518
513
  // Only sync with system if theme is 'default'
519
- if (theme !== 'default') return;
514
+ if (theme !== "default") return;
520
515
 
521
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
516
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
522
517
 
523
518
  const handleChange = (e: MediaQueryListEvent) => {
524
- updateTheme(e.matches ? 'dark' : 'light');
519
+ updateTheme(e.matches ? "dark" : "light");
525
520
  };
526
521
 
527
522
  // Set initial value
528
- updateTheme(mediaQuery.matches ? 'dark' : 'light');
523
+ updateTheme(mediaQuery.matches ? "dark" : "light");
529
524
 
530
525
  // Listen for changes
531
- mediaQuery.addEventListener('change', handleChange);
526
+ mediaQuery.addEventListener("change", handleChange);
532
527
 
533
- return () => mediaQuery.removeEventListener('change', handleChange);
528
+ return () => mediaQuery.removeEventListener("change", handleChange);
534
529
  }, [theme, updateTheme]);
535
530
  }
536
531
  ```
@@ -540,7 +535,7 @@ export function useSystemTheme() {
540
535
  ```tsx
541
536
  // components/ThemedImage.tsx
542
537
  import { useTheme } from "@/providers/ThemeProvider";
543
- import Image from 'next/image';
538
+ import Image from "next/image";
544
539
 
545
540
  interface ThemedImageProps {
546
541
  lightSrc: string;
@@ -561,7 +556,7 @@ export function ThemedImage({
561
556
 
562
557
  return (
563
558
  <Image
564
- src={theme === 'dark' ? darkSrc : lightSrc}
559
+ src={theme === "dark" ? darkSrc : lightSrc}
565
560
  alt={alt}
566
561
  width={width}
567
562
  height={height}
@@ -575,33 +570,33 @@ export function ThemedImage({
575
570
 
576
571
  ```tsx
577
572
  // __tests__/DarkMode.test.tsx
578
- import { render } from '@testing-library/react';
573
+ import { render } from "@testing-library/react";
579
574
  import { ThemeProvider } from "@/providers/ThemeProvider";
580
- import userEvent from '@testing-library/user-event';
581
- import DarkModeToggle from '../components/DarkModeToggle';
575
+ import userEvent from "@testing-library/user-event";
576
+ import DarkModeToggle from "../components/DarkModeToggle";
582
577
 
583
- describe('Dark Mode', () => {
584
- it('switches between light and dark themes', async () => {
578
+ describe("Dark Mode", () => {
579
+ it("switches between light and dark themes", async () => {
585
580
  const user = userEvent.setup();
586
581
 
587
582
  const { getByRole } = render(
588
583
  <ThemeProvider defaultTheme="light">
589
584
  <DarkModeToggle />
590
- </ThemeProvider>
585
+ </ThemeProvider>,
591
586
  );
592
587
 
593
- const toggle = getByRole('button');
588
+ const toggle = getByRole("button");
594
589
 
595
590
  // Initial theme is light
596
- expect(document.documentElement.getAttribute('data-theme')).toBe('light');
591
+ expect(document.documentElement.getAttribute("data-theme")).toBe("light");
597
592
 
598
593
  // Click to switch to dark
599
594
  await user.click(toggle);
600
- expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
595
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
601
596
 
602
597
  // Click to switch back to light
603
598
  await user.click(toggle);
604
- expect(document.documentElement.getAttribute('data-theme')).toBe('light');
599
+ expect(document.documentElement.getAttribute("data-theme")).toBe("light");
605
600
  });
606
601
  });
607
602
  ```
@@ -616,9 +611,9 @@ Ensure your application is accessible to all users.
616
611
 
617
612
  ```tsx
618
613
  // components/AccessibleMenu.tsx
619
- 'use client';
614
+ "use client";
620
615
 
621
- import { useState, useRef, useEffect } from 'react';
616
+ import { useState, useRef, useEffect } from "react";
622
617
  import { Button } from "@/components/Button";
623
618
 
624
619
  export default function AccessibleMenu() {
@@ -626,7 +621,7 @@ export default function AccessibleMenu() {
626
621
  const [focusedIndex, setFocusedIndex] = useState(0);
627
622
  const menuRef = useRef<HTMLDivElement>(null);
628
623
 
629
- const menuItems = ['Profile', 'Settings', 'Logout'];
624
+ const menuItems = ["Profile", "Settings", "Logout"];
630
625
 
631
626
  useEffect(() => {
632
627
  if (isOpen) {
@@ -637,22 +632,22 @@ export default function AccessibleMenu() {
637
632
 
638
633
  const handleKeyDown = (e: React.KeyboardEvent) => {
639
634
  switch (e.key) {
640
- case 'ArrowDown':
635
+ case "ArrowDown":
641
636
  e.preventDefault();
642
637
  setFocusedIndex((prev) => (prev + 1) % menuItems.length);
643
638
  break;
644
- case 'ArrowUp':
639
+ case "ArrowUp":
645
640
  e.preventDefault();
646
641
  setFocusedIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
647
642
  break;
648
- case 'Escape':
643
+ case "Escape":
649
644
  setIsOpen(false);
650
645
  break;
651
- case 'Home':
646
+ case "Home":
652
647
  e.preventDefault();
653
648
  setFocusedIndex(0);
654
649
  break;
655
- case 'End':
650
+ case "End":
656
651
  e.preventDefault();
657
652
  setFocusedIndex(menuItems.length - 1);
658
653
  break;
@@ -717,17 +712,14 @@ export default function AccessibleMenu() {
717
712
  // components/AccessibleForm.tsx
718
713
  import { Button } from "@/components/Button";
719
714
  import { LabelField } from "@/components/LabelField";
720
- import { useState } from 'react';
715
+ import { useState } from "react";
721
716
 
722
717
  export default function AccessibleForm() {
723
- const [email, setEmail] = useState('');
724
- const [error, setError] = useState('');
718
+ const [email, setEmail] = useState("");
719
+ const [error, setError] = useState("");
725
720
 
726
721
  return (
727
- <form
728
- aria-label="Contact form"
729
- onSubmit={(e) => e.preventDefault()}
730
- >
722
+ <form aria-label="Contact form" onSubmit={(e) => e.preventDefault()}>
731
723
  <LabelField
732
724
  theme="light"
733
725
  label="Email"
@@ -736,7 +728,7 @@ export default function AccessibleForm() {
736
728
  value={email}
737
729
  onChange={(e) => setEmail(e.target.value)}
738
730
  aria-invalid={!!error}
739
- aria-describedby={error ? 'email-error' : undefined}
731
+ aria-describedby={error ? "email-error" : undefined}
740
732
  />
741
733
 
742
734
  {error && (
@@ -749,12 +741,7 @@ export default function AccessibleForm() {
749
741
  </p>
750
742
  )}
751
743
 
752
- <Button
753
- theme="light"
754
- variant="PrimeStyle"
755
- type="submit"
756
- aria-label="Submit contact form"
757
- >
744
+ <Button theme="light" variant="PrimeStyle" type="submit" aria-label="Submit contact form">
758
745
  Submit
759
746
  </Button>
760
747
  </form>
@@ -766,7 +753,7 @@ export default function AccessibleForm() {
766
753
 
767
754
  ```tsx
768
755
  // hooks/useFocusTrap.ts
769
- import { useEffect, useRef } from 'react';
756
+ import { useEffect, useRef } from "react";
770
757
 
771
758
  export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
772
759
  const ref = useRef<T>(null);
@@ -778,14 +765,14 @@ export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
778
765
  if (!element) return;
779
766
 
780
767
  const focusableElements = element.querySelectorAll(
781
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
768
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
782
769
  );
783
770
 
784
771
  const firstElement = focusableElements[0] as HTMLElement;
785
772
  const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
786
773
 
787
774
  const handleTabKey = (e: KeyboardEvent) => {
788
- if (e.key !== 'Tab') return;
775
+ if (e.key !== "Tab") return;
789
776
 
790
777
  if (e.shiftKey && document.activeElement === firstElement) {
791
778
  e.preventDefault();
@@ -796,11 +783,11 @@ export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
796
783
  }
797
784
  };
798
785
 
799
- element.addEventListener('keydown', handleTabKey);
786
+ element.addEventListener("keydown", handleTabKey);
800
787
  firstElement?.focus();
801
788
 
802
789
  return () => {
803
- element.removeEventListener('keydown', handleTabKey);
790
+ element.removeEventListener("keydown", handleTabKey);
804
791
  };
805
792
  }, [isActive]);
806
793
 
@@ -820,11 +807,7 @@ interface AccessibleButtonProps {
820
807
  children: React.ReactNode;
821
808
  }
822
809
 
823
- export function AccessibleButton({
824
- onClick,
825
- isLoading,
826
- children,
827
- }: AccessibleButtonProps) {
810
+ export function AccessibleButton({ onClick, isLoading, children }: AccessibleButtonProps) {
828
811
  return (
829
812
  <Button
830
813
  theme="light"
@@ -832,7 +815,7 @@ export function AccessibleButton({
832
815
  onClick={onClick}
833
816
  disabled={isLoading}
834
817
  aria-busy={isLoading}
835
- aria-label={isLoading ? 'Loading...' : undefined}
818
+ aria-label={isLoading ? "Loading..." : undefined}
836
819
  >
837
820
  {isLoading ? (
838
821
  <>
@@ -869,34 +852,34 @@ export type FormTouched<T> = Partial<Record<keyof T, boolean>>;
869
852
 
870
853
  ```tsx
871
854
  // components/TypeSafeForm.tsx
872
- import { useState } from 'react';
855
+ import { useState } from "react";
873
856
  import { Button } from "@/components/Button";
874
857
  import { LabelField } from "@/components/LabelField";
875
858
  import { toast } from "@/components/Toast";
876
- import type { ContactForm, FormErrors, FormTouched } from '../types/forms';
859
+ import type { ContactForm, FormErrors, FormTouched } from "../types/forms";
877
860
 
878
861
  export default function TypeSafeForm() {
879
862
  const [formData, setFormData] = useState<ContactForm>({
880
- name: '',
881
- email: '',
882
- message: '',
863
+ name: "",
864
+ email: "",
865
+ message: "",
883
866
  });
884
867
 
885
868
  const [errors, setErrors] = useState<FormErrors<ContactForm>>({});
886
869
  const [touched, setTouched] = useState<FormTouched<ContactForm>>({});
887
870
 
888
- const handleChange = <K extends keyof ContactForm>(field: K) => (
889
- e: React.ChangeEvent<HTMLInputElement>
890
- ) => {
891
- setFormData((prev) => ({
892
- ...prev,
893
- [field]: e.target.value,
894
- }));
895
- };
871
+ const handleChange =
872
+ <K extends keyof ContactForm>(field: K) =>
873
+ (e: React.ChangeEvent<HTMLInputElement>) => {
874
+ setFormData((prev) => ({
875
+ ...prev,
876
+ [field]: e.target.value,
877
+ }));
878
+ };
896
879
 
897
880
  const handleSubmit = (e: React.FormEvent) => {
898
881
  e.preventDefault();
899
- toast.success('Form submitted!');
882
+ toast.success("Form submitted!");
900
883
  };
901
884
 
902
885
  return (
@@ -905,7 +888,7 @@ export default function TypeSafeForm() {
905
888
  theme="light"
906
889
  label="Name"
907
890
  value={formData.name}
908
- onChange={handleChange('name')}
891
+ onChange={handleChange("name")}
909
892
  errorMessage={touched.name ? errors.name : undefined}
910
893
  />
911
894
 
@@ -929,17 +912,11 @@ interface ListProps<T> {
929
912
  keyExtractor: (item: T) => string;
930
913
  }
931
914
 
932
- export function TypedList<T>({
933
- items,
934
- renderItem,
935
- keyExtractor,
936
- }: ListProps<T>) {
915
+ export function TypedList<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
937
916
  return (
938
917
  <div className="space-y-2">
939
918
  {items.map((item) => (
940
- <div key={keyExtractor(item)}>
941
- {renderItem(item)}
942
- </div>
919
+ <div key={keyExtractor(item)}>{renderItem(item)}</div>
943
920
  ))}
944
921
  </div>
945
922
  );
@@ -950,14 +927,11 @@ export function TypedList<T>({
950
927
 
951
928
  ```tsx
952
929
  // hooks/useTypedLocalStorage.ts
953
- import { useState, useEffect } from 'react';
930
+ import { useState, useEffect } from "react";
954
931
 
955
- export function useTypedLocalStorage<T>(
956
- key: string,
957
- initialValue: T
958
- ): [T, (value: T) => void] {
932
+ export function useTypedLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
959
933
  const [storedValue, setStoredValue] = useState<T>(() => {
960
- if (typeof window === 'undefined') return initialValue;
934
+ if (typeof window === "undefined") return initialValue;
961
935
 
962
936
  try {
963
937
  const item = window.localStorage.getItem(key);
@@ -971,7 +945,7 @@ export function useTypedLocalStorage<T>(
971
945
  const setValue = (value: T) => {
972
946
  try {
973
947
  setStoredValue(value);
974
- if (typeof window !== 'undefined') {
948
+ if (typeof window !== "undefined") {
975
949
  window.localStorage.setItem(key, JSON.stringify(value));
976
950
  }
977
951
  } catch (error) {
@@ -998,11 +972,11 @@ export interface ExtendedButtonProps extends ButtonProps {
998
972
  ```tsx
999
973
  // components/ExtendedButton.tsx
1000
974
  import { Button } from "@/components/Button";
1001
- import type { ExtendedButtonProps } from '../types/components';
975
+ import type { ExtendedButtonProps } from "../types/components";
1002
976
 
1003
977
  export function ExtendedButton({
1004
978
  isLoading,
1005
- loadingText = 'Loading...',
979
+ loadingText = "Loading...",
1006
980
  children,
1007
981
  ...props
1008
982
  }: ExtendedButtonProps) {
@@ -1018,19 +992,19 @@ export function ExtendedButton({
1018
992
 
1019
993
  ```tsx
1020
994
  // utils/typeGuards.ts
1021
- import type { User } from '../types';
995
+ import type { User } from "../types";
1022
996
 
1023
997
  export function isValidEmail(value: unknown): value is string {
1024
- return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
998
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
1025
999
  }
1026
1000
 
1027
1001
  export function isUser(value: unknown): value is User {
1028
1002
  return (
1029
- typeof value === 'object' &&
1003
+ typeof value === "object" &&
1030
1004
  value !== null &&
1031
- 'id' in value &&
1032
- 'email' in value &&
1033
- 'name' in value
1005
+ "id" in value &&
1006
+ "email" in value &&
1007
+ "name" in value
1034
1008
  );
1035
1009
  }
1036
1010
  ```