torch-glare 2.4.0 → 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.
- package/README.md +8 -0
- package/apps/lib/components/ActionButton.tsx +7 -12
- package/apps/lib/components/ActionsGroup.tsx +4 -15
- package/apps/lib/components/AlertDialog.tsx +173 -191
- package/apps/lib/components/Avatar.tsx +13 -16
- package/apps/lib/components/Badge.tsx +1 -3
- package/apps/lib/components/BadgeField.tsx +76 -71
- package/apps/lib/components/Breadcrumb.tsx +49 -85
- package/apps/lib/components/Button.tsx +21 -20
- package/apps/lib/components/ButtonGroup.tsx +31 -41
- package/apps/lib/components/Calendar.tsx +51 -29
- package/apps/lib/components/Card.tsx +40 -24
- package/apps/lib/components/Checkbox.tsx +9 -16
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/ContextMenu.tsx +97 -96
- package/apps/lib/components/CountBadge.tsx +4 -6
- package/apps/lib/components/DataTable.tsx +196 -216
- package/apps/lib/components/DataViews/DataViewRadio.tsx +1 -6
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +13 -47
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +1 -3
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +5 -12
- package/apps/lib/components/DataViews/FilterPanel.tsx +155 -149
- package/apps/lib/components/DataViews/HeaderSearch.tsx +1 -5
- package/apps/lib/components/DataViews/InboxView.tsx +22 -68
- package/apps/lib/components/DataViews/InboxViewCard.tsx +68 -77
- package/apps/lib/components/DataViews/KanbanView.tsx +8 -25
- package/apps/lib/components/DataViews/PanelControls.tsx +3 -13
- package/apps/lib/components/DataViews/SettingsPanel.tsx +83 -89
- package/apps/lib/components/DataViews/TableView.tsx +8 -28
- package/apps/lib/components/DataViews/TreeView.tsx +12 -40
- package/apps/lib/components/DataViews/badgeAdapter.ts +32 -28
- package/apps/lib/components/DataViews/fieldRenderers.tsx +106 -141
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +30 -23
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +39 -32
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +15 -15
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +69 -58
- package/apps/lib/components/DataViews/index.ts +26 -26
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +12 -16
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +28 -31
- package/apps/lib/components/DataViews/types.ts +104 -115
- package/apps/lib/components/DatePicker.tsx +200 -196
- package/apps/lib/components/Dialog.tsx +92 -105
- package/apps/lib/components/Divider.tsx +21 -24
- package/apps/lib/components/Drawer.tsx +124 -142
- package/apps/lib/components/DropdownMenu.tsx +131 -171
- package/apps/lib/components/FieldHint.tsx +4 -11
- package/apps/lib/components/Form.tsx +146 -155
- package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
- package/apps/lib/components/FormBuilder/context.ts +71 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
- package/apps/lib/components/FormBuilder/header.tsx +105 -0
- package/apps/lib/components/FormBuilder/index.ts +37 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
- package/apps/lib/components/FormBuilder/submit.tsx +41 -0
- package/apps/lib/components/FormBuilder/types.ts +264 -0
- package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
- package/apps/lib/components/FormRenderer/index.ts +9 -0
- package/apps/lib/components/FormRenderer/types.ts +79 -0
- package/apps/lib/components/FormStepper.tsx +9 -39
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/HeaderBar.tsx +1 -9
- package/apps/lib/components/ImageAttachment.tsx +106 -93
- package/apps/lib/components/InnerLabelField.tsx +20 -16
- package/apps/lib/components/Input.tsx +171 -172
- package/apps/lib/components/InputField.tsx +44 -45
- package/apps/lib/components/InputOTP.tsx +68 -69
- package/apps/lib/components/Label.tsx +61 -58
- package/apps/lib/components/LabelField.tsx +4 -5
- package/apps/lib/components/LabeledCheckBox.tsx +8 -12
- package/apps/lib/components/LabeledRadio.tsx +7 -5
- package/apps/lib/components/LinkButton.tsx +2 -9
- package/apps/lib/components/LoginButton.tsx +4 -9
- package/apps/lib/components/PasswordLevel.tsx +5 -5
- package/apps/lib/components/Popover.tsx +15 -33
- package/apps/lib/components/ProfileMenu.tsx +67 -72
- package/apps/lib/components/Radio.tsx +1 -5
- package/apps/lib/components/RadioCard.tsx +11 -28
- package/apps/lib/components/ScrollArea.tsx +45 -43
- package/apps/lib/components/SearchField.tsx +49 -39
- package/apps/lib/components/SearchableSelect.tsx +16 -16
- package/apps/lib/components/SearchableTable.tsx +108 -118
- package/apps/lib/components/SearchableTree.tsx +469 -0
- package/apps/lib/components/SearchableTreeDialog.tsx +493 -0
- package/apps/lib/components/SectionBlock.tsx +19 -23
- package/apps/lib/components/Select.tsx +80 -186
- package/apps/lib/components/SimpleSelect.tsx +196 -187
- package/apps/lib/components/Skeleton.tsx +9 -12
- package/apps/lib/components/SlideDatePicker.tsx +215 -166
- package/apps/lib/components/SpinLoading.tsx +33 -49
- package/apps/lib/components/Stepper.tsx +13 -56
- package/apps/lib/components/Switch.tsx +8 -10
- package/apps/lib/components/TabFormItem.tsx +3 -12
- package/apps/lib/components/TabSwitch.tsx +7 -34
- package/apps/lib/components/Table.tsx +60 -75
- package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +17 -52
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +15 -48
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +156 -113
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +2 -7
- package/apps/lib/components/Textarea.tsx +9 -13
- package/apps/lib/components/Timeline.tsx +21 -41
- package/apps/lib/components/Toast.tsx +83 -83
- package/apps/lib/components/Toggle.tsx +112 -114
- package/apps/lib/components/ToggleButton.tsx +1 -1
- package/apps/lib/components/Tooltip.tsx +24 -19
- package/apps/lib/components/TransparentLabel.tsx +61 -62
- package/apps/lib/components/TreeDropDown.tsx +86 -52
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +15 -48
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +16 -21
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +9 -20
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +3 -3
- package/apps/lib/components/TreeFolder/icons.tsx +14 -17
- package/apps/lib/components/TreeFolder/index.ts +15 -8
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +47 -52
- package/apps/lib/components/TreeFolder/types.ts +32 -32
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +117 -123
- package/apps/lib/hooks/useActiveTreeItem.ts +52 -55
- package/apps/lib/hooks/useClickOutside.ts +21 -20
- package/apps/lib/hooks/useDataViewsState.ts +86 -80
- package/apps/lib/hooks/useIsMobile.ts +12 -12
- package/apps/lib/hooks/useResize.ts +54 -65
- package/apps/lib/hooks/useTagSelection.ts +201 -201
- package/apps/lib/layouts/CNLayout.tsx +181 -140
- package/apps/lib/layouts/DataViewCard.tsx +4 -18
- package/apps/lib/layouts/FieldSection.tsx +2 -6
- package/apps/lib/layouts/TreeSubLayout.tsx +24 -38
- package/apps/lib/providers/ThemeProvider.tsx +8 -23
- package/apps/lib/registry.json +997 -0
- package/apps/lib/tsconfig.json +9 -0
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/cn.ts +1 -1
- package/apps/lib/utils/color.ts +175 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +46 -44
- package/apps/lib/utils/dataViews/fieldUtils.ts +70 -71
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +129 -122
- package/apps/lib/utils/dataViews/pathUtils.ts +74 -67
- package/apps/lib/utils/dataViews/rangeUtils.ts +121 -112
- package/apps/lib/utils/dataViews/treeUtils.ts +204 -211
- package/apps/lib/utils/dateFormat.ts +64 -54
- package/apps/lib/utils/resize.ts +21 -25
- package/apps/lib/utils/types.ts +13 -13
- package/dist/bin/index.js +8 -2
- package/dist/bin/index.js.map +1 -1
- package/dist/src/shared/getAvailableFiles.d.ts.map +1 -1
- package/dist/src/shared/getAvailableFiles.js +6 -1
- package/dist/src/shared/getAvailableFiles.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +7 -0
- package/dist/src/shared/loadRegistry.d.ts.map +1 -0
- package/dist/src/shared/loadRegistry.js +31 -0
- package/dist/src/shared/loadRegistry.js.map +1 -0
- package/dist/src/shared/resolveInstallPlan.d.ts +12 -0
- package/dist/src/shared/resolveInstallPlan.d.ts.map +1 -0
- package/dist/src/shared/resolveInstallPlan.js +39 -0
- package/dist/src/shared/resolveInstallPlan.js.map +1 -0
- package/dist/src/types/main.d.ts +24 -0
- package/dist/src/types/main.d.ts.map +1 -1
- package/docs/README.md +10 -2
- package/docs/components/action-button.md +30 -24
- package/docs/components/actions-group.md +26 -20
- package/docs/components/alert-dialog.md +45 -45
- package/docs/components/avatar.md +6 -2
- package/docs/components/badge-field.md +6 -2
- package/docs/components/badge.md +7 -1
- package/docs/components/breadcrumb.md +15 -40
- package/docs/components/button-group.md +13 -10
- package/docs/components/button.md +58 -33
- package/docs/components/calendar.md +26 -19
- package/docs/components/card.md +23 -18
- package/docs/components/chart-block-tool.md +129 -0
- package/docs/components/checkbox.md +16 -8
- package/docs/components/cn-layout.md +7 -1
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/context-menu.md +18 -25
- package/docs/components/count-badge.md +7 -1
- package/docs/components/data-table.md +45 -33
- package/docs/components/data-views-config-panel.md +15 -11
- package/docs/components/data-views-layout.md +13 -16
- package/docs/components/date-picker.md +30 -23
- package/docs/components/dialog.md +43 -42
- package/docs/components/divider.md +10 -4
- package/docs/components/drawer.md +161 -135
- package/docs/components/dropdown-menu.md +25 -34
- package/docs/components/field-hint.md +37 -25
- package/docs/components/field-section.md +9 -3
- package/docs/components/form-builder.md +270 -0
- package/docs/components/form-renderer.md +228 -0
- package/docs/components/form-stepper.md +6 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/form.md +17 -20
- package/docs/components/header-bar.md +6 -0
- package/docs/components/image-attachment.md +43 -45
- package/docs/components/inbox-view.md +16 -5
- package/docs/components/inner-label-field.md +34 -24
- package/docs/components/input-field.md +18 -10
- package/docs/components/input-otp.md +14 -11
- package/docs/components/input.md +22 -16
- package/docs/components/kanban-view.md +15 -5
- package/docs/components/label-field.md +32 -22
- package/docs/components/label.md +43 -32
- package/docs/components/labeled-check-box.md +16 -8
- package/docs/components/labeled-radio.md +18 -11
- package/docs/components/link-button.md +7 -1
- package/docs/components/login-button.md +8 -2
- package/docs/components/password-level.md +37 -24
- package/docs/components/popover.md +26 -23
- package/docs/components/profile-menu.md +38 -27
- package/docs/components/radio-card.md +16 -9
- package/docs/components/radio.md +16 -8
- package/docs/components/scroll-area.md +6 -2
- package/docs/components/search-field.md +14 -6
- package/docs/components/searchable-select.md +19 -14
- package/docs/components/searchable-table.md +18 -13
- package/docs/components/searchable-tree-dialog.md +190 -0
- package/docs/components/searchable-tree.md +200 -0
- package/docs/components/section-block.md +12 -4
- package/docs/components/select.md +23 -18
- package/docs/components/simple-select.md +14 -10
- package/docs/components/skeleton.md +43 -33
- package/docs/components/slide-date-picker.md +37 -29
- package/docs/components/spin-loading.md +32 -23
- package/docs/components/stepper.md +6 -0
- package/docs/components/switch.md +14 -6
- package/docs/components/tab-form-item.md +7 -1
- package/docs/components/tab-switch.md +13 -6
- package/docs/components/table-dnd-wrapper.md +107 -0
- package/docs/components/table-view.md +15 -5
- package/docs/components/table.md +28 -28
- package/docs/components/text-editor.md +37 -13
- package/docs/components/textarea.md +15 -7
- package/docs/components/timeline.md +6 -0
- package/docs/components/toast.md +35 -26
- package/docs/components/toggle-button.md +24 -21
- package/docs/components/toggle.md +23 -15
- package/docs/components/tooltip.md +42 -30
- package/docs/components/transparent-label.md +29 -20
- package/docs/components/tree-drop-down.md +31 -23
- package/docs/components/tree-sub-layout.md +6 -2
- package/docs/components/tree-view.md +16 -5
- package/docs/explanation/architecture.md +54 -0
- package/docs/explanation/design-system.md +52 -0
- package/docs/how-to/data-views-from-backend-response.md +6 -4
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +408 -0
- package/docs/how-to/guides.md +177 -197
- package/docs/migration/changelog.md +38 -0
- package/docs/reference/cli.md +87 -0
- package/docs/reference/components.md +11 -12
- package/docs/reference/hooks.md +8 -8
- package/docs/reference/providers.md +22 -22
- package/docs/reference/tailwind-plugins.md +2 -2
- package/docs/reference/theme.md +85 -0
- package/docs/reference/types.md +4 -4
- package/docs/reference/utilities.md +19 -26
- package/docs/tutorials/building-first-form.md +170 -172
- package/docs/tutorials/component-composition.md +34 -50
- package/docs/tutorials/getting-started.md +2 -2
- package/docs/tutorials/theming-basics.md +13 -10
- package/package.json +31 -3
- package/dist/src/commands/block.d.ts +0 -5
- package/dist/src/commands/block.d.ts.map +0 -1
- package/dist/src/commands/block.js +0 -255
- package/dist/src/commands/block.js.map +0 -1
- package/dist/src/shared/configureFonts.d.ts +0 -6
- package/dist/src/shared/configureFonts.d.ts.map +0 -1
- package/dist/src/shared/configureFonts.js +0 -106
- package/dist/src/shared/configureFonts.js.map +0 -1
- package/dist/src/shared/configureGlobalCss.d.ts +0 -6
- package/dist/src/shared/configureGlobalCss.d.ts.map +0 -1
- package/dist/src/shared/configureGlobalCss.js +0 -154
- package/dist/src/shared/configureGlobalCss.js.map +0 -1
- package/dist/src/shared/configureTailwind.d.ts +0 -7
- package/dist/src/shared/configureTailwind.d.ts.map +0 -1
- package/dist/src/shared/configureTailwind.js +0 -163
- package/dist/src/shared/configureTailwind.js.map +0 -1
- package/dist/src/shared/detectFramework.d.ts +0 -23
- package/dist/src/shared/detectFramework.d.ts.map +0 -1
- package/dist/src/shared/detectFramework.js +0 -119
- package/dist/src/shared/detectFramework.js.map +0 -1
- package/dist/src/shared/installBaseUtils.d.ts +0 -5
- package/dist/src/shared/installBaseUtils.d.ts.map +0 -1
- package/dist/src/shared/installBaseUtils.js +0 -79
- package/dist/src/shared/installBaseUtils.js.map +0 -1
- package/dist/src/shared/resolveAliases.d.ts +0 -24
- package/dist/src/shared/resolveAliases.d.ts.map +0 -1
- package/dist/src/shared/resolveAliases.js +0 -98
- package/dist/src/shared/resolveAliases.js.map +0 -1
package/docs/how-to/guides.md
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
50
|
-
require(
|
|
51
|
-
require(
|
|
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: #
|
|
67
|
-
--content-presentation-action-light-primary: #
|
|
68
|
-
--border-presentation-action-primary: #
|
|
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: #
|
|
73
|
-
--content-presentation-action-light-primary: #
|
|
74
|
-
--border-presentation-action-primary: #
|
|
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
|
```
|
|
@@ -80,17 +86,17 @@ export default {
|
|
|
80
86
|
|
|
81
87
|
```tsx
|
|
82
88
|
// components/BrandButton.tsx
|
|
83
|
-
import { Button } from
|
|
84
|
-
import { cn } from
|
|
89
|
+
import { Button } from "@/components/Button";
|
|
90
|
+
import { cn } from "@/utils/cn";
|
|
85
91
|
|
|
86
92
|
export function BrandButton({ children, ...props }: any) {
|
|
87
93
|
return (
|
|
88
94
|
<Button
|
|
89
95
|
{...props}
|
|
90
96
|
className={cn(
|
|
91
|
-
|
|
92
|
-
|
|
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}
|
|
@@ -103,22 +109,22 @@ export function BrandButton({ children, ...props }: any) {
|
|
|
103
109
|
|
|
104
110
|
```tsx
|
|
105
111
|
// hooks/useCustomTheme.ts
|
|
106
|
-
import { useTheme } from
|
|
107
|
-
import { useEffect } from
|
|
112
|
+
import { useTheme } from "@/providers/ThemeProvider";
|
|
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 ===
|
|
115
|
-
document.documentElement.classList.add(
|
|
120
|
+
if (theme === "light") {
|
|
121
|
+
document.documentElement.classList.add("brand-light-theme");
|
|
116
122
|
} else {
|
|
117
|
-
document.documentElement.classList.add(
|
|
123
|
+
document.documentElement.classList.add("brand-dark-theme");
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
return () => {
|
|
121
|
-
document.documentElement.classList.remove(
|
|
127
|
+
document.documentElement.classList.remove("brand-light-theme", "brand-dark-theme");
|
|
122
128
|
};
|
|
123
129
|
}, [theme]);
|
|
124
130
|
|
|
@@ -130,14 +136,12 @@ export function useCustomTheme() {
|
|
|
130
136
|
|
|
131
137
|
```tsx
|
|
132
138
|
// providers/BrandThemeProvider.tsx
|
|
133
|
-
import { ThemeProvider } from
|
|
139
|
+
import { ThemeProvider } from "@/providers/ThemeProvider";
|
|
134
140
|
|
|
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
|
|
151
|
-
import { ThemeProvider } from
|
|
152
|
-
import { BrandButton } from
|
|
154
|
+
import { render } from "@testing-library/react";
|
|
155
|
+
import { ThemeProvider } from "@/providers/ThemeProvider";
|
|
156
|
+
import { BrandButton } from "../components/BrandButton";
|
|
153
157
|
|
|
154
|
-
describe(
|
|
155
|
-
it(
|
|
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(
|
|
163
|
-
expect(button).toHaveClass(
|
|
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
|
|
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 =
|
|
272
|
+
export const required = (message = "This field is required") => {
|
|
271
273
|
return (value: any) => {
|
|
272
|
-
if (!value || (typeof value ===
|
|
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 =
|
|
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,52 +329,47 @@ export const matchField = (otherField: string, message?: string) => {
|
|
|
327
329
|
|
|
328
330
|
```tsx
|
|
329
331
|
// components/RegistrationForm.tsx
|
|
330
|
-
|
|
332
|
+
"use client";
|
|
331
333
|
|
|
332
|
-
import { useFormValidation } from
|
|
333
|
-
import { required, email, minLength } from
|
|
334
|
-
import {
|
|
334
|
+
import { useFormValidation } from "../hooks/useFormValidation";
|
|
335
|
+
import { required, email, minLength } from "../utils/validationRules";
|
|
336
|
+
import { Button } from "@/components/Button";
|
|
337
|
+
import { LabelField } from "@/components/LabelField";
|
|
338
|
+
import { toast } from "@/components/Toast";
|
|
335
339
|
|
|
336
340
|
export default function RegistrationForm() {
|
|
337
|
-
const {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return 'Passwords must match';
|
|
359
|
-
}
|
|
360
|
-
return undefined;
|
|
361
|
-
},
|
|
362
|
-
],
|
|
363
|
-
}
|
|
364
|
-
);
|
|
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
|
+
);
|
|
365
362
|
|
|
366
363
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
367
364
|
e.preventDefault();
|
|
368
365
|
|
|
369
366
|
if (!validateForm()) {
|
|
370
|
-
toast.error(
|
|
367
|
+
toast.error("Please fix validation errors");
|
|
371
368
|
return;
|
|
372
369
|
}
|
|
373
370
|
|
|
374
371
|
// Submit form
|
|
375
|
-
toast.success(
|
|
372
|
+
toast.success("Registration successful!");
|
|
376
373
|
reset();
|
|
377
374
|
};
|
|
378
375
|
|
|
@@ -384,8 +381,8 @@ export default function RegistrationForm() {
|
|
|
384
381
|
requiredLabel="*"
|
|
385
382
|
type="email"
|
|
386
383
|
value={values.email}
|
|
387
|
-
onChange={handleChange(
|
|
388
|
-
onBlur={handleBlur(
|
|
384
|
+
onChange={handleChange("email")}
|
|
385
|
+
onBlur={handleBlur("email")}
|
|
389
386
|
errorMessage={touched.email ? errors.email : undefined}
|
|
390
387
|
/>
|
|
391
388
|
|
|
@@ -395,8 +392,8 @@ export default function RegistrationForm() {
|
|
|
395
392
|
requiredLabel="*"
|
|
396
393
|
type="password"
|
|
397
394
|
value={values.password}
|
|
398
|
-
onChange={handleChange(
|
|
399
|
-
onBlur={handleBlur(
|
|
395
|
+
onChange={handleChange("password")}
|
|
396
|
+
onBlur={handleBlur("password")}
|
|
400
397
|
errorMessage={touched.password ? errors.password : undefined}
|
|
401
398
|
/>
|
|
402
399
|
|
|
@@ -406,8 +403,8 @@ export default function RegistrationForm() {
|
|
|
406
403
|
requiredLabel="*"
|
|
407
404
|
type="password"
|
|
408
405
|
value={values.confirmPassword}
|
|
409
|
-
onChange={handleChange(
|
|
410
|
-
onBlur={handleBlur(
|
|
406
|
+
onChange={handleChange("confirmPassword")}
|
|
407
|
+
onBlur={handleBlur("confirmPassword")}
|
|
411
408
|
errorMessage={touched.confirmPassword ? errors.confirmPassword : undefined}
|
|
412
409
|
/>
|
|
413
410
|
|
|
@@ -423,7 +420,7 @@ export default function RegistrationForm() {
|
|
|
423
420
|
|
|
424
421
|
```tsx
|
|
425
422
|
// Example: Check if email is available
|
|
426
|
-
export const emailAvailable = (message =
|
|
423
|
+
export const emailAvailable = (message = "Email already taken") => {
|
|
427
424
|
return async (value: string) => {
|
|
428
425
|
if (!value) return undefined;
|
|
429
426
|
|
|
@@ -435,7 +432,7 @@ export const emailAvailable = (message = 'Email already taken') => {
|
|
|
435
432
|
return message;
|
|
436
433
|
}
|
|
437
434
|
} catch (error) {
|
|
438
|
-
console.error(
|
|
435
|
+
console.error("Email validation error:", error);
|
|
439
436
|
}
|
|
440
437
|
|
|
441
438
|
return undefined;
|
|
@@ -453,10 +450,11 @@ Implement comprehensive dark mode support.
|
|
|
453
450
|
|
|
454
451
|
```tsx
|
|
455
452
|
// components/DarkModeToggle.tsx
|
|
456
|
-
|
|
453
|
+
"use client";
|
|
457
454
|
|
|
458
|
-
import {
|
|
459
|
-
import {
|
|
455
|
+
import { Button } from "@/components/Button";
|
|
456
|
+
import { useTheme } from "@/providers/ThemeProvider";
|
|
457
|
+
import { useEffect, useState } from "react";
|
|
460
458
|
|
|
461
459
|
export default function DarkModeToggle() {
|
|
462
460
|
const { theme, updateTheme } = useTheme();
|
|
@@ -469,7 +467,7 @@ export default function DarkModeToggle() {
|
|
|
469
467
|
|
|
470
468
|
if (!mounted) {
|
|
471
469
|
return (
|
|
472
|
-
<Button theme="light" variant="
|
|
470
|
+
<Button theme="light" variant="PrimeContStyle" buttonType="icon">
|
|
473
471
|
<div className="w-5 h-5" />
|
|
474
472
|
</Button>
|
|
475
473
|
);
|
|
@@ -478,12 +476,12 @@ export default function DarkModeToggle() {
|
|
|
478
476
|
return (
|
|
479
477
|
<Button
|
|
480
478
|
theme={theme as any}
|
|
481
|
-
variant="
|
|
479
|
+
variant="PrimeContStyle"
|
|
482
480
|
buttonType="icon"
|
|
483
|
-
onClick={() => updateTheme(theme ===
|
|
484
|
-
aria-label={`Switch to ${theme ===
|
|
481
|
+
onClick={() => updateTheme(theme === "light" ? "dark" : "light")}
|
|
482
|
+
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
|
485
483
|
>
|
|
486
|
-
{theme ===
|
|
484
|
+
{theme === "light" ? (
|
|
487
485
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
488
486
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
|
489
487
|
</svg>
|
|
@@ -505,29 +503,29 @@ export default function DarkModeToggle() {
|
|
|
505
503
|
|
|
506
504
|
```tsx
|
|
507
505
|
// hooks/useSystemTheme.ts
|
|
508
|
-
import { useEffect } from
|
|
509
|
-
import { useTheme } from
|
|
506
|
+
import { useEffect } from "react";
|
|
507
|
+
import { useTheme } from "@/providers/ThemeProvider";
|
|
510
508
|
|
|
511
509
|
export function useSystemTheme() {
|
|
512
510
|
const { theme, updateTheme } = useTheme();
|
|
513
511
|
|
|
514
512
|
useEffect(() => {
|
|
515
513
|
// Only sync with system if theme is 'default'
|
|
516
|
-
if (theme !==
|
|
514
|
+
if (theme !== "default") return;
|
|
517
515
|
|
|
518
|
-
const mediaQuery = window.matchMedia(
|
|
516
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
519
517
|
|
|
520
518
|
const handleChange = (e: MediaQueryListEvent) => {
|
|
521
|
-
updateTheme(e.matches ?
|
|
519
|
+
updateTheme(e.matches ? "dark" : "light");
|
|
522
520
|
};
|
|
523
521
|
|
|
524
522
|
// Set initial value
|
|
525
|
-
updateTheme(mediaQuery.matches ?
|
|
523
|
+
updateTheme(mediaQuery.matches ? "dark" : "light");
|
|
526
524
|
|
|
527
525
|
// Listen for changes
|
|
528
|
-
mediaQuery.addEventListener(
|
|
526
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
529
527
|
|
|
530
|
-
return () => mediaQuery.removeEventListener(
|
|
528
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
531
529
|
}, [theme, updateTheme]);
|
|
532
530
|
}
|
|
533
531
|
```
|
|
@@ -536,8 +534,8 @@ export function useSystemTheme() {
|
|
|
536
534
|
|
|
537
535
|
```tsx
|
|
538
536
|
// components/ThemedImage.tsx
|
|
539
|
-
import { useTheme } from
|
|
540
|
-
import Image from
|
|
537
|
+
import { useTheme } from "@/providers/ThemeProvider";
|
|
538
|
+
import Image from "next/image";
|
|
541
539
|
|
|
542
540
|
interface ThemedImageProps {
|
|
543
541
|
lightSrc: string;
|
|
@@ -558,7 +556,7 @@ export function ThemedImage({
|
|
|
558
556
|
|
|
559
557
|
return (
|
|
560
558
|
<Image
|
|
561
|
-
src={theme ===
|
|
559
|
+
src={theme === "dark" ? darkSrc : lightSrc}
|
|
562
560
|
alt={alt}
|
|
563
561
|
width={width}
|
|
564
562
|
height={height}
|
|
@@ -572,33 +570,33 @@ export function ThemedImage({
|
|
|
572
570
|
|
|
573
571
|
```tsx
|
|
574
572
|
// __tests__/DarkMode.test.tsx
|
|
575
|
-
import { render } from
|
|
576
|
-
import { ThemeProvider } from
|
|
577
|
-
import userEvent from
|
|
578
|
-
import DarkModeToggle from
|
|
573
|
+
import { render } from "@testing-library/react";
|
|
574
|
+
import { ThemeProvider } from "@/providers/ThemeProvider";
|
|
575
|
+
import userEvent from "@testing-library/user-event";
|
|
576
|
+
import DarkModeToggle from "../components/DarkModeToggle";
|
|
579
577
|
|
|
580
|
-
describe(
|
|
581
|
-
it(
|
|
578
|
+
describe("Dark Mode", () => {
|
|
579
|
+
it("switches between light and dark themes", async () => {
|
|
582
580
|
const user = userEvent.setup();
|
|
583
581
|
|
|
584
582
|
const { getByRole } = render(
|
|
585
583
|
<ThemeProvider defaultTheme="light">
|
|
586
584
|
<DarkModeToggle />
|
|
587
|
-
</ThemeProvider
|
|
585
|
+
</ThemeProvider>,
|
|
588
586
|
);
|
|
589
587
|
|
|
590
|
-
const toggle = getByRole(
|
|
588
|
+
const toggle = getByRole("button");
|
|
591
589
|
|
|
592
590
|
// Initial theme is light
|
|
593
|
-
expect(document.documentElement.getAttribute(
|
|
591
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
594
592
|
|
|
595
593
|
// Click to switch to dark
|
|
596
594
|
await user.click(toggle);
|
|
597
|
-
expect(document.documentElement.getAttribute(
|
|
595
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
598
596
|
|
|
599
597
|
// Click to switch back to light
|
|
600
598
|
await user.click(toggle);
|
|
601
|
-
expect(document.documentElement.getAttribute(
|
|
599
|
+
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
602
600
|
});
|
|
603
601
|
});
|
|
604
602
|
```
|
|
@@ -613,17 +611,17 @@ Ensure your application is accessible to all users.
|
|
|
613
611
|
|
|
614
612
|
```tsx
|
|
615
613
|
// components/AccessibleMenu.tsx
|
|
616
|
-
|
|
614
|
+
"use client";
|
|
617
615
|
|
|
618
|
-
import { useState, useRef, useEffect } from
|
|
619
|
-
import { Button } from
|
|
616
|
+
import { useState, useRef, useEffect } from "react";
|
|
617
|
+
import { Button } from "@/components/Button";
|
|
620
618
|
|
|
621
619
|
export default function AccessibleMenu() {
|
|
622
620
|
const [isOpen, setIsOpen] = useState(false);
|
|
623
621
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
624
622
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
625
623
|
|
|
626
|
-
const menuItems = [
|
|
624
|
+
const menuItems = ["Profile", "Settings", "Logout"];
|
|
627
625
|
|
|
628
626
|
useEffect(() => {
|
|
629
627
|
if (isOpen) {
|
|
@@ -634,22 +632,22 @@ export default function AccessibleMenu() {
|
|
|
634
632
|
|
|
635
633
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
636
634
|
switch (e.key) {
|
|
637
|
-
case
|
|
635
|
+
case "ArrowDown":
|
|
638
636
|
e.preventDefault();
|
|
639
637
|
setFocusedIndex((prev) => (prev + 1) % menuItems.length);
|
|
640
638
|
break;
|
|
641
|
-
case
|
|
639
|
+
case "ArrowUp":
|
|
642
640
|
e.preventDefault();
|
|
643
641
|
setFocusedIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
|
|
644
642
|
break;
|
|
645
|
-
case
|
|
643
|
+
case "Escape":
|
|
646
644
|
setIsOpen(false);
|
|
647
645
|
break;
|
|
648
|
-
case
|
|
646
|
+
case "Home":
|
|
649
647
|
e.preventDefault();
|
|
650
648
|
setFocusedIndex(0);
|
|
651
649
|
break;
|
|
652
|
-
case
|
|
650
|
+
case "End":
|
|
653
651
|
e.preventDefault();
|
|
654
652
|
setFocusedIndex(menuItems.length - 1);
|
|
655
653
|
break;
|
|
@@ -712,18 +710,16 @@ export default function AccessibleMenu() {
|
|
|
712
710
|
|
|
713
711
|
```tsx
|
|
714
712
|
// components/AccessibleForm.tsx
|
|
715
|
-
import {
|
|
716
|
-
import {
|
|
713
|
+
import { Button } from "@/components/Button";
|
|
714
|
+
import { LabelField } from "@/components/LabelField";
|
|
715
|
+
import { useState } from "react";
|
|
717
716
|
|
|
718
717
|
export default function AccessibleForm() {
|
|
719
|
-
const [email, setEmail] = useState(
|
|
720
|
-
const [error, setError] = useState(
|
|
718
|
+
const [email, setEmail] = useState("");
|
|
719
|
+
const [error, setError] = useState("");
|
|
721
720
|
|
|
722
721
|
return (
|
|
723
|
-
<form
|
|
724
|
-
aria-label="Contact form"
|
|
725
|
-
onSubmit={(e) => e.preventDefault()}
|
|
726
|
-
>
|
|
722
|
+
<form aria-label="Contact form" onSubmit={(e) => e.preventDefault()}>
|
|
727
723
|
<LabelField
|
|
728
724
|
theme="light"
|
|
729
725
|
label="Email"
|
|
@@ -732,7 +728,7 @@ export default function AccessibleForm() {
|
|
|
732
728
|
value={email}
|
|
733
729
|
onChange={(e) => setEmail(e.target.value)}
|
|
734
730
|
aria-invalid={!!error}
|
|
735
|
-
aria-describedby={error ?
|
|
731
|
+
aria-describedby={error ? "email-error" : undefined}
|
|
736
732
|
/>
|
|
737
733
|
|
|
738
734
|
{error && (
|
|
@@ -745,12 +741,7 @@ export default function AccessibleForm() {
|
|
|
745
741
|
</p>
|
|
746
742
|
)}
|
|
747
743
|
|
|
748
|
-
<Button
|
|
749
|
-
theme="light"
|
|
750
|
-
variant="PrimeStyle"
|
|
751
|
-
type="submit"
|
|
752
|
-
aria-label="Submit contact form"
|
|
753
|
-
>
|
|
744
|
+
<Button theme="light" variant="PrimeStyle" type="submit" aria-label="Submit contact form">
|
|
754
745
|
Submit
|
|
755
746
|
</Button>
|
|
756
747
|
</form>
|
|
@@ -762,7 +753,7 @@ export default function AccessibleForm() {
|
|
|
762
753
|
|
|
763
754
|
```tsx
|
|
764
755
|
// hooks/useFocusTrap.ts
|
|
765
|
-
import { useEffect, useRef } from
|
|
756
|
+
import { useEffect, useRef } from "react";
|
|
766
757
|
|
|
767
758
|
export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
|
768
759
|
const ref = useRef<T>(null);
|
|
@@ -774,14 +765,14 @@ export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
|
|
774
765
|
if (!element) return;
|
|
775
766
|
|
|
776
767
|
const focusableElements = element.querySelectorAll(
|
|
777
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
768
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
778
769
|
);
|
|
779
770
|
|
|
780
771
|
const firstElement = focusableElements[0] as HTMLElement;
|
|
781
772
|
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
782
773
|
|
|
783
774
|
const handleTabKey = (e: KeyboardEvent) => {
|
|
784
|
-
if (e.key !==
|
|
775
|
+
if (e.key !== "Tab") return;
|
|
785
776
|
|
|
786
777
|
if (e.shiftKey && document.activeElement === firstElement) {
|
|
787
778
|
e.preventDefault();
|
|
@@ -792,11 +783,11 @@ export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
|
|
792
783
|
}
|
|
793
784
|
};
|
|
794
785
|
|
|
795
|
-
element.addEventListener(
|
|
786
|
+
element.addEventListener("keydown", handleTabKey);
|
|
796
787
|
firstElement?.focus();
|
|
797
788
|
|
|
798
789
|
return () => {
|
|
799
|
-
element.removeEventListener(
|
|
790
|
+
element.removeEventListener("keydown", handleTabKey);
|
|
800
791
|
};
|
|
801
792
|
}, [isActive]);
|
|
802
793
|
|
|
@@ -808,7 +799,7 @@ export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
|
|
808
799
|
|
|
809
800
|
```tsx
|
|
810
801
|
// components/AccessibleButton.tsx
|
|
811
|
-
import { Button } from
|
|
802
|
+
import { Button } from "@/components/Button";
|
|
812
803
|
|
|
813
804
|
interface AccessibleButtonProps {
|
|
814
805
|
onClick: () => void;
|
|
@@ -816,11 +807,7 @@ interface AccessibleButtonProps {
|
|
|
816
807
|
children: React.ReactNode;
|
|
817
808
|
}
|
|
818
809
|
|
|
819
|
-
export function AccessibleButton({
|
|
820
|
-
onClick,
|
|
821
|
-
isLoading,
|
|
822
|
-
children,
|
|
823
|
-
}: AccessibleButtonProps) {
|
|
810
|
+
export function AccessibleButton({ onClick, isLoading, children }: AccessibleButtonProps) {
|
|
824
811
|
return (
|
|
825
812
|
<Button
|
|
826
813
|
theme="light"
|
|
@@ -828,7 +815,7 @@ export function AccessibleButton({
|
|
|
828
815
|
onClick={onClick}
|
|
829
816
|
disabled={isLoading}
|
|
830
817
|
aria-busy={isLoading}
|
|
831
|
-
aria-label={isLoading ?
|
|
818
|
+
aria-label={isLoading ? "Loading..." : undefined}
|
|
832
819
|
>
|
|
833
820
|
{isLoading ? (
|
|
834
821
|
<>
|
|
@@ -865,32 +852,34 @@ export type FormTouched<T> = Partial<Record<keyof T, boolean>>;
|
|
|
865
852
|
|
|
866
853
|
```tsx
|
|
867
854
|
// components/TypeSafeForm.tsx
|
|
868
|
-
import { useState } from
|
|
869
|
-
import {
|
|
870
|
-
import
|
|
855
|
+
import { useState } from "react";
|
|
856
|
+
import { Button } from "@/components/Button";
|
|
857
|
+
import { LabelField } from "@/components/LabelField";
|
|
858
|
+
import { toast } from "@/components/Toast";
|
|
859
|
+
import type { ContactForm, FormErrors, FormTouched } from "../types/forms";
|
|
871
860
|
|
|
872
861
|
export default function TypeSafeForm() {
|
|
873
862
|
const [formData, setFormData] = useState<ContactForm>({
|
|
874
|
-
name:
|
|
875
|
-
email:
|
|
876
|
-
message:
|
|
863
|
+
name: "",
|
|
864
|
+
email: "",
|
|
865
|
+
message: "",
|
|
877
866
|
});
|
|
878
867
|
|
|
879
868
|
const [errors, setErrors] = useState<FormErrors<ContactForm>>({});
|
|
880
869
|
const [touched, setTouched] = useState<FormTouched<ContactForm>>({});
|
|
881
870
|
|
|
882
|
-
const handleChange =
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
};
|
|
890
879
|
|
|
891
880
|
const handleSubmit = (e: React.FormEvent) => {
|
|
892
881
|
e.preventDefault();
|
|
893
|
-
toast.success(
|
|
882
|
+
toast.success("Form submitted!");
|
|
894
883
|
};
|
|
895
884
|
|
|
896
885
|
return (
|
|
@@ -899,7 +888,7 @@ export default function TypeSafeForm() {
|
|
|
899
888
|
theme="light"
|
|
900
889
|
label="Name"
|
|
901
890
|
value={formData.name}
|
|
902
|
-
onChange={handleChange(
|
|
891
|
+
onChange={handleChange("name")}
|
|
903
892
|
errorMessage={touched.name ? errors.name : undefined}
|
|
904
893
|
/>
|
|
905
894
|
|
|
@@ -923,17 +912,11 @@ interface ListProps<T> {
|
|
|
923
912
|
keyExtractor: (item: T) => string;
|
|
924
913
|
}
|
|
925
914
|
|
|
926
|
-
export function TypedList<T>({
|
|
927
|
-
items,
|
|
928
|
-
renderItem,
|
|
929
|
-
keyExtractor,
|
|
930
|
-
}: ListProps<T>) {
|
|
915
|
+
export function TypedList<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
|
931
916
|
return (
|
|
932
917
|
<div className="space-y-2">
|
|
933
918
|
{items.map((item) => (
|
|
934
|
-
<div key={keyExtractor(item)}>
|
|
935
|
-
{renderItem(item)}
|
|
936
|
-
</div>
|
|
919
|
+
<div key={keyExtractor(item)}>{renderItem(item)}</div>
|
|
937
920
|
))}
|
|
938
921
|
</div>
|
|
939
922
|
);
|
|
@@ -944,14 +927,11 @@ export function TypedList<T>({
|
|
|
944
927
|
|
|
945
928
|
```tsx
|
|
946
929
|
// hooks/useTypedLocalStorage.ts
|
|
947
|
-
import { useState, useEffect } from
|
|
930
|
+
import { useState, useEffect } from "react";
|
|
948
931
|
|
|
949
|
-
export function useTypedLocalStorage<T>(
|
|
950
|
-
key: string,
|
|
951
|
-
initialValue: T
|
|
952
|
-
): [T, (value: T) => void] {
|
|
932
|
+
export function useTypedLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
953
933
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
954
|
-
if (typeof window ===
|
|
934
|
+
if (typeof window === "undefined") return initialValue;
|
|
955
935
|
|
|
956
936
|
try {
|
|
957
937
|
const item = window.localStorage.getItem(key);
|
|
@@ -965,7 +945,7 @@ export function useTypedLocalStorage<T>(
|
|
|
965
945
|
const setValue = (value: T) => {
|
|
966
946
|
try {
|
|
967
947
|
setStoredValue(value);
|
|
968
|
-
if (typeof window !==
|
|
948
|
+
if (typeof window !== "undefined") {
|
|
969
949
|
window.localStorage.setItem(key, JSON.stringify(value));
|
|
970
950
|
}
|
|
971
951
|
} catch (error) {
|
|
@@ -981,7 +961,7 @@ export function useTypedLocalStorage<T>(
|
|
|
981
961
|
|
|
982
962
|
```tsx
|
|
983
963
|
// types/components.ts
|
|
984
|
-
import type { ButtonProps } from
|
|
964
|
+
import type { ButtonProps } from "@/components/LoginButton";
|
|
985
965
|
|
|
986
966
|
export interface ExtendedButtonProps extends ButtonProps {
|
|
987
967
|
isLoading?: boolean;
|
|
@@ -991,12 +971,12 @@ export interface ExtendedButtonProps extends ButtonProps {
|
|
|
991
971
|
|
|
992
972
|
```tsx
|
|
993
973
|
// components/ExtendedButton.tsx
|
|
994
|
-
import { Button } from
|
|
995
|
-
import type { ExtendedButtonProps } from
|
|
974
|
+
import { Button } from "@/components/Button";
|
|
975
|
+
import type { ExtendedButtonProps } from "../types/components";
|
|
996
976
|
|
|
997
977
|
export function ExtendedButton({
|
|
998
978
|
isLoading,
|
|
999
|
-
loadingText =
|
|
979
|
+
loadingText = "Loading...",
|
|
1000
980
|
children,
|
|
1001
981
|
...props
|
|
1002
982
|
}: ExtendedButtonProps) {
|
|
@@ -1012,19 +992,19 @@ export function ExtendedButton({
|
|
|
1012
992
|
|
|
1013
993
|
```tsx
|
|
1014
994
|
// utils/typeGuards.ts
|
|
1015
|
-
import type { User } from
|
|
995
|
+
import type { User } from "../types";
|
|
1016
996
|
|
|
1017
997
|
export function isValidEmail(value: unknown): value is string {
|
|
1018
|
-
return typeof value ===
|
|
998
|
+
return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
1019
999
|
}
|
|
1020
1000
|
|
|
1021
1001
|
export function isUser(value: unknown): value is User {
|
|
1022
1002
|
return (
|
|
1023
|
-
typeof value ===
|
|
1003
|
+
typeof value === "object" &&
|
|
1024
1004
|
value !== null &&
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1005
|
+
"id" in value &&
|
|
1006
|
+
"email" in value &&
|
|
1007
|
+
"name" in value
|
|
1028
1008
|
);
|
|
1029
1009
|
}
|
|
1030
1010
|
```
|