torch-glare 2.5.6 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/bin/index.js +9 -24
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts +2 -2
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +4 -70
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/hook.d.ts +5 -4
- package/dist/src/commands/hook.d.ts.map +1 -1
- package/dist/src/commands/hook.js +7 -75
- package/dist/src/commands/hook.js.map +1 -1
- package/dist/src/commands/init.js +1 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/layout.d.ts +5 -4
- package/dist/src/commands/layout.d.ts.map +1 -1
- package/dist/src/commands/layout.js +7 -75
- package/dist/src/commands/layout.js.map +1 -1
- package/dist/src/commands/provider.d.ts +5 -4
- package/dist/src/commands/provider.d.ts.map +1 -1
- package/dist/src/commands/provider.js +7 -75
- package/dist/src/commands/provider.js.map +1 -1
- package/dist/src/commands/update.d.ts.map +1 -1
- package/dist/src/commands/update.js +78 -43
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/commands/utils.d.ts +5 -4
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +7 -80
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/addFromRegistry.d.ts +16 -0
- package/dist/src/shared/addFromRegistry.d.ts.map +1 -0
- package/dist/src/shared/addFromRegistry.js +88 -0
- package/dist/src/shared/addFromRegistry.js.map +1 -0
- package/dist/src/shared/getInstallCommand.d.ts.map +1 -1
- package/dist/src/shared/getInstallCommand.js +5 -0
- package/dist/src/shared/getInstallCommand.js.map +1 -1
- package/dist/src/shared/installDependencies.d.ts +0 -10
- package/dist/src/shared/installDependencies.d.ts.map +1 -1
- package/dist/src/shared/installDependencies.js +0 -17
- package/dist/src/shared/installDependencies.js.map +1 -1
- package/dist/src/shared/installFromPlan.d.ts +7 -5
- package/dist/src/shared/installFromPlan.d.ts.map +1 -1
- package/dist/src/shared/installFromPlan.js +103 -36
- package/dist/src/shared/installFromPlan.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +12 -3
- package/dist/src/shared/loadRegistry.d.ts.map +1 -1
- package/dist/src/shared/loadRegistry.js +19 -20
- package/dist/src/shared/loadRegistry.js.map +1 -1
- package/dist/src/shared/registryClient.d.ts +19 -0
- package/dist/src/shared/registryClient.d.ts.map +1 -0
- package/dist/src/shared/registryClient.js +128 -0
- package/dist/src/shared/registryClient.js.map +1 -0
- package/dist/src/shared/resolveEntry.d.ts +10 -7
- package/dist/src/shared/resolveEntry.d.ts.map +1 -1
- package/dist/src/shared/resolveEntry.js +18 -13
- package/dist/src/shared/resolveEntry.js.map +1 -1
- package/dist/src/shared/suggestOtherCommand.d.ts +7 -1
- package/dist/src/shared/suggestOtherCommand.d.ts.map +1 -1
- package/dist/src/shared/suggestOtherCommand.js +29 -23
- package/dist/src/shared/suggestOtherCommand.js.map +1 -1
- package/dist/src/types/main.d.ts +31 -2
- package/dist/src/types/main.d.ts.map +1 -1
- package/package.json +6 -11
- package/apps/lib/components/ActionButton.tsx +0 -93
- package/apps/lib/components/ActionsGroup.tsx +0 -27
- package/apps/lib/components/AlertDialog.tsx +0 -204
- package/apps/lib/components/Avatar.tsx +0 -46
- package/apps/lib/components/Badge.tsx +0 -250
- package/apps/lib/components/BadgeField.tsx +0 -403
- package/apps/lib/components/Breadcrumb.tsx +0 -280
- package/apps/lib/components/Button.tsx +0 -325
- package/apps/lib/components/ButtonGroup.tsx +0 -190
- package/apps/lib/components/Calendar.tsx +0 -115
- package/apps/lib/components/Card.tsx +0 -95
- package/apps/lib/components/Checkbox.tsx +0 -42
- package/apps/lib/components/ColorPicker.tsx +0 -441
- package/apps/lib/components/ConclusionHeader.tsx +0 -148
- package/apps/lib/components/ContextMenu.tsx +0 -571
- package/apps/lib/components/CountBadge.tsx +0 -52
- package/apps/lib/components/DataTable.tsx +0 -213
- package/apps/lib/components/DataViews/badge.ts +0 -49
- package/apps/lib/components/DataViews/cell.tsx +0 -324
- package/apps/lib/components/DataViews/context.ts +0 -144
- package/apps/lib/components/DataViews/data-views.tsx +0 -395
- package/apps/lib/components/DataViews/filters/children.tsx +0 -98
- package/apps/lib/components/DataViews/filters/custom.tsx +0 -34
- package/apps/lib/components/DataViews/filters/filters.tsx +0 -157
- package/apps/lib/components/DataViews/filters/index.ts +0 -4
- package/apps/lib/components/DataViews/filters/labelled.tsx +0 -20
- package/apps/lib/components/DataViews/filters/presets.tsx +0 -65
- package/apps/lib/components/DataViews/filters/sync.tsx +0 -35
- package/apps/lib/components/DataViews/filters/values.ts +0 -173
- package/apps/lib/components/DataViews/header.tsx +0 -220
- package/apps/lib/components/DataViews/hooks/index.ts +0 -5
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +0 -22
- package/apps/lib/components/DataViews/hooks/useControllable.ts +0 -52
- package/apps/lib/components/DataViews/index.ts +0 -88
- package/apps/lib/components/DataViews/panel/columns.tsx +0 -153
- package/apps/lib/components/DataViews/panel/controls.tsx +0 -106
- package/apps/lib/components/DataViews/panel/index.ts +0 -3
- package/apps/lib/components/DataViews/panel/panel.tsx +0 -164
- package/apps/lib/components/DataViews/panel/saved-views.tsx +0 -67
- package/apps/lib/components/DataViews/panel/section.tsx +0 -81
- package/apps/lib/components/DataViews/panel/sort.tsx +0 -42
- package/apps/lib/components/DataViews/panel/tab.tsx +0 -31
- package/apps/lib/components/DataViews/slots.ts +0 -72
- package/apps/lib/components/DataViews/states.tsx +0 -73
- package/apps/lib/components/DataViews/types.ts +0 -510
- package/apps/lib/components/DataViews/views/board-view.tsx +0 -381
- package/apps/lib/components/DataViews/views/card-rows.tsx +0 -36
- package/apps/lib/components/DataViews/views/inbox-view.tsx +0 -260
- package/apps/lib/components/DataViews/views/pane-views.tsx +0 -196
- package/apps/lib/components/DataViews/views/table-view.tsx +0 -493
- package/apps/lib/components/DataViews/views/tree-view.tsx +0 -367
- package/apps/lib/components/DatePicker.tsx +0 -232
- package/apps/lib/components/Dialog.tsx +0 -116
- package/apps/lib/components/Divider.tsx +0 -28
- package/apps/lib/components/Drawer.tsx +0 -451
- package/apps/lib/components/DropdownMenu.tsx +0 -554
- package/apps/lib/components/FieldHint.tsx +0 -69
- package/apps/lib/components/Form.tsx +0 -182
- package/apps/lib/components/FormBuilder/context.ts +0 -85
- package/apps/lib/components/FormBuilder/field-kind.ts +0 -28
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +0 -60
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +0 -59
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +0 -11
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +0 -32
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +0 -74
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +0 -168
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +0 -39
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +0 -132
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +0 -31
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +0 -109
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +0 -31
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +0 -114
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +0 -157
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +0 -67
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +0 -41
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +0 -336
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +0 -213
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +0 -43
- package/apps/lib/components/FormBuilder/fields/countries.ts +0 -303
- package/apps/lib/components/FormBuilder/fields/index.ts +0 -25
- package/apps/lib/components/FormBuilder/form-builder.tsx +0 -198
- package/apps/lib/components/FormBuilder/index.ts +0 -31
- package/apps/lib/components/FormBuilder/numberFormat.ts +0 -16
- package/apps/lib/components/FormBuilder/submit.tsx +0 -67
- package/apps/lib/components/FormBuilder/types.ts +0 -316
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -250
- package/apps/lib/components/FormRenderer/detail.tsx +0 -256
- package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -358
- package/apps/lib/components/FormRenderer/header.tsx +0 -88
- package/apps/lib/components/FormRenderer/index.ts +0 -17
- package/apps/lib/components/FormRenderer/notch-action.tsx +0 -64
- package/apps/lib/components/FormRenderer/section.tsx +0 -39
- package/apps/lib/components/FormRenderer/stepper.tsx +0 -393
- package/apps/lib/components/FormRenderer/types.ts +0 -115
- package/apps/lib/components/FormSummary.tsx +0 -282
- package/apps/lib/components/HeaderBar.tsx +0 -117
- package/apps/lib/components/ImageAttachment.tsx +0 -209
- package/apps/lib/components/InnerLabelField.tsx +0 -154
- package/apps/lib/components/Input.tsx +0 -224
- package/apps/lib/components/InputField.tsx +0 -147
- package/apps/lib/components/InputOTP.tsx +0 -84
- package/apps/lib/components/Label.tsx +0 -129
- package/apps/lib/components/LabelField.tsx +0 -76
- package/apps/lib/components/LabeledCheckBox.tsx +0 -51
- package/apps/lib/components/LabeledRadio.tsx +0 -51
- package/apps/lib/components/LinkButton.tsx +0 -94
- package/apps/lib/components/LoginButton.tsx +0 -51
- package/apps/lib/components/PasswordLevel.tsx +0 -58
- package/apps/lib/components/Popover.tsx +0 -312
- package/apps/lib/components/ProfileMenu.tsx +0 -85
- package/apps/lib/components/Radio.tsx +0 -59
- package/apps/lib/components/RadioCard.tsx +0 -50
- package/apps/lib/components/ScrollArea.tsx +0 -54
- package/apps/lib/components/SearchField.tsx +0 -62
- package/apps/lib/components/SearchableSelect.tsx +0 -318
- package/apps/lib/components/SearchableTable.tsx +0 -344
- package/apps/lib/components/SearchableTree.tsx +0 -485
- package/apps/lib/components/SearchableTreeDialog.tsx +0 -502
- package/apps/lib/components/SectionBlock.tsx +0 -168
- package/apps/lib/components/Select.tsx +0 -347
- package/apps/lib/components/SimpleSelect.tsx +0 -225
- package/apps/lib/components/Skeleton.tsx +0 -12
- package/apps/lib/components/SlideDatePicker.tsx +0 -255
- package/apps/lib/components/SpinLoading.tsx +0 -174
- package/apps/lib/components/Stepper.tsx +0 -480
- package/apps/lib/components/Switch.tsx +0 -82
- package/apps/lib/components/TabFormItem.tsx +0 -153
- package/apps/lib/components/TabSwitch.tsx +0 -172
- package/apps/lib/components/Table.tsx +0 -597
- package/apps/lib/components/TextEditor/ChartBlockTool.ts +0 -676
- package/apps/lib/components/TextEditor/RichTextField.tsx +0 -46
- package/apps/lib/components/TextEditor/TableDnDWrapper.ts +0 -462
- package/apps/lib/components/TextEditor/TextEditor.tsx +0 -1164
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +0 -429
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +0 -70
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +0 -50
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +0 -48
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +0 -98
- package/apps/lib/components/TextEditor/editorjs.d.ts +0 -60
- package/apps/lib/components/TextEditor/index.ts +0 -7
- package/apps/lib/components/TextEditor/markdownParser.ts +0 -346
- package/apps/lib/components/Textarea.tsx +0 -117
- package/apps/lib/components/Timeline.tsx +0 -263
- package/apps/lib/components/Toast.tsx +0 -87
- package/apps/lib/components/Toggle.tsx +0 -125
- package/apps/lib/components/ToggleButton.tsx +0 -140
- package/apps/lib/components/Tooltip.tsx +0 -116
- package/apps/lib/components/TransparentLabel.tsx +0 -71
- package/apps/lib/components/TreeDropDown.tsx +0 -115
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +0 -380
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +0 -75
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +0 -354
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +0 -60
- package/apps/lib/components/TreeFolder/icons.tsx +0 -60
- package/apps/lib/components/TreeFolder/index.ts +0 -24
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +0 -109
- package/apps/lib/components/TreeFolder/types.ts +0 -77
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +0 -118
- package/apps/lib/hooks/useActiveTreeItem.ts +0 -61
- package/apps/lib/hooks/useClickOutside.ts +0 -27
- package/apps/lib/hooks/useDragDrop.tsx +0 -365
- package/apps/lib/hooks/useHtmlDir.ts +0 -31
- package/apps/lib/hooks/useInfiniteScroll.ts +0 -108
- package/apps/lib/hooks/useIsMobile.ts +0 -21
- package/apps/lib/hooks/useResize.ts +0 -67
- package/apps/lib/hooks/useTagSelection.ts +0 -300
- package/apps/lib/layouts/CNLayout.tsx +0 -372
- package/apps/lib/layouts/DataViewCard.tsx +0 -62
- package/apps/lib/layouts/FieldSection.tsx +0 -91
- package/apps/lib/layouts/TreeSubLayout.tsx +0 -173
- package/apps/lib/providers/ThemeProvider.tsx +0 -84
- package/apps/lib/registry.json +0 -1217
- package/apps/lib/tsconfig.json +0 -9
- package/apps/lib/tsconfig.tsbuildinfo +0 -1
- package/apps/lib/utils/cn.ts +0 -6
- package/apps/lib/utils/color.ts +0 -175
- package/apps/lib/utils/dataViews/path.ts +0 -67
- package/apps/lib/utils/dataViews/query.ts +0 -73
- package/apps/lib/utils/dataViews/types.ts +0 -187
- package/apps/lib/utils/dateFormat.ts +0 -108
- package/apps/lib/utils/resize.ts +0 -31
- package/apps/lib/utils/scroller.ts +0 -26
- package/apps/lib/utils/types.ts +0 -16
- package/docs/BLOCKS.md +0 -129
- package/docs/CHANGELOG-1.1.16.md +0 -108
- package/docs/Cover.png +0 -0
- package/docs/README.md +0 -215
- package/docs/components/action-button.md +0 -638
- package/docs/components/actions-group.md +0 -708
- package/docs/components/alert-dialog.md +0 -920
- package/docs/components/avatar.md +0 -709
- package/docs/components/badge-field.md +0 -738
- package/docs/components/badge.md +0 -301
- package/docs/components/breadcrumb.md +0 -930
- package/docs/components/button-group.md +0 -650
- package/docs/components/button.md +0 -639
- package/docs/components/calendar.md +0 -555
- package/docs/components/card.md +0 -622
- package/docs/components/chart-block-tool.md +0 -129
- package/docs/components/checkbox.md +0 -699
- package/docs/components/cn-layout.md +0 -854
- package/docs/components/color-picker.md +0 -101
- package/docs/components/conclusion-header.md +0 -80
- package/docs/components/context-menu.md +0 -483
- package/docs/components/count-badge.md +0 -560
- package/docs/components/data-table.md +0 -854
- package/docs/components/data-views/backend-response.md +0 -324
- package/docs/components/data-views/examples/a11y-rtl.md +0 -250
- package/docs/components/data-views/examples/api-orders-route.md +0 -130
- package/docs/components/data-views/examples/fields.md +0 -362
- package/docs/components/data-views/examples/filters.md +0 -307
- package/docs/components/data-views/examples/inbox-routing.md +0 -218
- package/docs/components/data-views/examples/index.md +0 -29
- package/docs/components/data-views/examples/overview.md +0 -244
- package/docs/components/data-views/examples/panel.md +0 -212
- package/docs/components/data-views/examples/scale.md +0 -231
- package/docs/components/data-views/examples/server-side.md +0 -210
- package/docs/components/data-views/examples/state.md +0 -250
- package/docs/components/data-views/examples/tree-custom.md +0 -388
- package/docs/components/data-views/examples/view-registry.md +0 -313
- package/docs/components/data-views/examples/views.md +0 -534
- package/docs/components/data-views/guide.md +0 -405
- package/docs/components/data-views/index.md +0 -1521
- package/docs/components/data-views/migration.md +0 -81
- package/docs/components/date-picker.md +0 -857
- package/docs/components/dialog.md +0 -929
- package/docs/components/divider.md +0 -766
- package/docs/components/drawer.md +0 -654
- package/docs/components/dropdown-menu.md +0 -668
- package/docs/components/field-hint.md +0 -898
- package/docs/components/field-section.md +0 -781
- package/docs/components/form-builder.md +0 -284
- package/docs/components/form-renderer.md +0 -396
- package/docs/components/form-summary.md +0 -138
- package/docs/components/form.md +0 -785
- package/docs/components/header-bar.md +0 -188
- package/docs/components/image-attachment.md +0 -906
- package/docs/components/inner-label-field.md +0 -819
- package/docs/components/input-field.md +0 -623
- package/docs/components/input-otp.md +0 -756
- package/docs/components/input.md +0 -634
- package/docs/components/label-field.md +0 -770
- package/docs/components/label.md +0 -761
- package/docs/components/labeled-check-box.md +0 -642
- package/docs/components/labeled-radio.md +0 -796
- package/docs/components/link-button.md +0 -685
- package/docs/components/login-button.md +0 -872
- package/docs/components/password-level.md +0 -803
- package/docs/components/popover.md +0 -649
- package/docs/components/profile-menu.md +0 -656
- package/docs/components/radio-card.md +0 -801
- package/docs/components/radio.md +0 -735
- package/docs/components/scroll-area.md +0 -691
- package/docs/components/search-field.md +0 -702
- package/docs/components/searchable-select.md +0 -368
- package/docs/components/searchable-table.md +0 -438
- package/docs/components/searchable-tree-dialog.md +0 -190
- package/docs/components/searchable-tree.md +0 -200
- package/docs/components/section-block.md +0 -483
- package/docs/components/select.md +0 -709
- package/docs/components/simple-select.md +0 -741
- package/docs/components/skeleton.md +0 -706
- package/docs/components/slide-date-picker.md +0 -837
- package/docs/components/spin-loading.md +0 -696
- package/docs/components/stepper.md +0 -316
- package/docs/components/switch.md +0 -770
- package/docs/components/tab-form-item.md +0 -773
- package/docs/components/tab-switch.md +0 -170
- package/docs/components/table-dnd-wrapper.md +0 -107
- package/docs/components/table.md +0 -1088
- package/docs/components/text-editor.md +0 -735
- package/docs/components/textarea.md +0 -681
- package/docs/components/timeline.md +0 -254
- package/docs/components/toast.md +0 -773
- package/docs/components/toggle-button.md +0 -643
- package/docs/components/toggle.md +0 -882
- package/docs/components/tooltip.md +0 -513
- package/docs/components/transparent-label.md +0 -721
- package/docs/components/tree-drop-down.md +0 -740
- package/docs/components/tree-folder.md +0 -110
- package/docs/components/tree-sub-layout.md +0 -707
- package/docs/explanation/architecture.md +0 -54
- package/docs/explanation/design-system.md +0 -52
- package/docs/how-to/form-and-list-recipes.md +0 -386
- package/docs/how-to/forms-with-form-builder.md +0 -472
- package/docs/how-to/guides.md +0 -1027
- package/docs/migration/changelog.md +0 -60
- package/docs/migration/form-builder-2.5.2.md +0 -113
- package/docs/reference/cli.md +0 -112
- package/docs/reference/components.md +0 -572
- package/docs/reference/hooks.md +0 -1523
- package/docs/reference/providers.md +0 -866
- package/docs/reference/tailwind-plugins.md +0 -942
- package/docs/reference/theme.md +0 -85
- package/docs/reference/types.md +0 -1016
- package/docs/reference/utilities.md +0 -726
- package/docs/tutorials/building-first-form.md +0 -846
- package/docs/tutorials/component-composition.md +0 -882
- package/docs/tutorials/getting-started.md +0 -302
- package/docs/tutorials/theming-basics.md +0 -785
package/docs/how-to/guides.md
DELETED
|
@@ -1,1027 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: How-to Guides
|
|
3
|
-
description: Practical guides for common tasks and patterns with TORCH Glare Components Library.
|
|
4
|
-
category: How-to
|
|
5
|
-
tags: [guides, best-practices, customization, accessibility, typescript, validation, dark-mode]
|
|
6
|
-
related:
|
|
7
|
-
- Getting Started
|
|
8
|
-
- Theming Basics
|
|
9
|
-
- Component Composition
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# How-to Guides
|
|
13
|
-
|
|
14
|
-
Practical, step-by-step guides for accomplishing common tasks with TORCH Glare.
|
|
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
|
-
|
|
22
|
-
## Table of Contents
|
|
23
|
-
|
|
24
|
-
1. [Custom Themes](#custom-themes)
|
|
25
|
-
2. [Form Validation](#form-validation)
|
|
26
|
-
3. [Dark Mode Implementation](#dark-mode-implementation)
|
|
27
|
-
4. [Accessibility Best Practices](#accessibility-best-practices)
|
|
28
|
-
5. [TypeScript Integration](#typescript-integration)
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Custom Themes
|
|
33
|
-
|
|
34
|
-
Learn how to create and apply custom themes to match your brand.
|
|
35
|
-
|
|
36
|
-
### Create a Custom Color Palette
|
|
37
|
-
|
|
38
|
-
**Step 1**: Define your custom colors in `tailwind.config.ts`
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
// tailwind.config.ts
|
|
42
|
-
export default {
|
|
43
|
-
theme: {
|
|
44
|
-
extend: {
|
|
45
|
-
colors: {
|
|
46
|
-
"brand-primary": "#4200FF",
|
|
47
|
-
"brand-secondary": "#5215CC",
|
|
48
|
-
"brand-accent": "#CFBEFF",
|
|
49
|
-
"brand-dark": "#0D0F4E",
|
|
50
|
-
"brand-light": "#F9F9F9",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
plugins: [
|
|
55
|
-
require("mapping-color-system"),
|
|
56
|
-
require("glare-torch-mode"),
|
|
57
|
-
require("glare-typography"),
|
|
58
|
-
],
|
|
59
|
-
} satisfies Config;
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Step 2**: Override theme CSS variables
|
|
63
|
-
|
|
64
|
-
```css
|
|
65
|
-
/* globals.css */
|
|
66
|
-
@tailwind base;
|
|
67
|
-
@tailwind components;
|
|
68
|
-
@tailwind utilities;
|
|
69
|
-
|
|
70
|
-
@layer base {
|
|
71
|
-
[data-theme="light"] {
|
|
72
|
-
--background-presentation-action-primary: #4200ff;
|
|
73
|
-
--content-presentation-action-light-primary: #ffffff;
|
|
74
|
-
--border-presentation-action-primary: #5215cc;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
[data-theme="dark"] {
|
|
78
|
-
--background-presentation-action-primary: #5215cc;
|
|
79
|
-
--content-presentation-action-light-primary: #f9f9f9;
|
|
80
|
-
--border-presentation-action-primary: #cfbeff;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Step 3**: Create custom component variants
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
// components/BrandButton.tsx
|
|
89
|
-
import { Button } from "@/components/Button";
|
|
90
|
-
import { cn } from "@/utils/cn";
|
|
91
|
-
|
|
92
|
-
export function BrandButton({ children, ...props }: any) {
|
|
93
|
-
return (
|
|
94
|
-
<Button
|
|
95
|
-
{...props}
|
|
96
|
-
className={cn(
|
|
97
|
-
"bg-brand-primary hover:bg-brand-secondary",
|
|
98
|
-
"text-white border-brand-accent",
|
|
99
|
-
props.className,
|
|
100
|
-
)}
|
|
101
|
-
>
|
|
102
|
-
{children}
|
|
103
|
-
</Button>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Create a Custom Theme Hook
|
|
109
|
-
|
|
110
|
-
```tsx
|
|
111
|
-
// hooks/useCustomTheme.ts
|
|
112
|
-
import { useTheme } from "@/providers/ThemeProvider";
|
|
113
|
-
import { useEffect } from "react";
|
|
114
|
-
|
|
115
|
-
export function useCustomTheme() {
|
|
116
|
-
const { theme, updateTheme } = useTheme();
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
// Apply custom theme classes
|
|
120
|
-
if (theme === "light") {
|
|
121
|
-
document.documentElement.classList.add("brand-light-theme");
|
|
122
|
-
} else {
|
|
123
|
-
document.documentElement.classList.add("brand-dark-theme");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return () => {
|
|
127
|
-
document.documentElement.classList.remove("brand-light-theme", "brand-dark-theme");
|
|
128
|
-
};
|
|
129
|
-
}, [theme]);
|
|
130
|
-
|
|
131
|
-
return { theme, updateTheme };
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Custom Theme Provider
|
|
136
|
-
|
|
137
|
-
```tsx
|
|
138
|
-
// providers/BrandThemeProvider.tsx
|
|
139
|
-
import { ThemeProvider } from "@/providers/ThemeProvider";
|
|
140
|
-
|
|
141
|
-
export function BrandThemeProvider({ children }: { children: React.ReactNode }) {
|
|
142
|
-
return (
|
|
143
|
-
<ThemeProvider defaultTheme="light" defaultThemeMode="TORCH">
|
|
144
|
-
<div className="brand-theme-wrapper">{children}</div>
|
|
145
|
-
</ThemeProvider>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Testing Custom Themes
|
|
151
|
-
|
|
152
|
-
```tsx
|
|
153
|
-
// __tests__/BrandButton.test.tsx
|
|
154
|
-
import { render } from "@testing-library/react";
|
|
155
|
-
import { ThemeProvider } from "@/providers/ThemeProvider";
|
|
156
|
-
import { BrandButton } from "../components/BrandButton";
|
|
157
|
-
|
|
158
|
-
describe("BrandButton", () => {
|
|
159
|
-
it("applies custom brand colors", () => {
|
|
160
|
-
const { container } = render(
|
|
161
|
-
<ThemeProvider defaultTheme="light">
|
|
162
|
-
<BrandButton>Click me</BrandButton>
|
|
163
|
-
</ThemeProvider>,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const button = container.querySelector("button");
|
|
167
|
-
expect(button).toHaveClass("bg-brand-primary");
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## Form Validation
|
|
175
|
-
|
|
176
|
-
Implement comprehensive form validation with error handling.
|
|
177
|
-
|
|
178
|
-
### Basic Validation Setup
|
|
179
|
-
|
|
180
|
-
```tsx
|
|
181
|
-
// hooks/useFormValidation.ts
|
|
182
|
-
import { useState, useCallback } from "react";
|
|
183
|
-
|
|
184
|
-
type ValidationRules<T> = {
|
|
185
|
-
[K in keyof T]?: Array<(value: T[K]) => string | undefined>;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export function useFormValidation<T extends Record<string, any>>(
|
|
189
|
-
initialValues: T,
|
|
190
|
-
rules: ValidationRules<T>,
|
|
191
|
-
) {
|
|
192
|
-
const [values, setValues] = useState<T>(initialValues);
|
|
193
|
-
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
|
|
194
|
-
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
|
|
195
|
-
|
|
196
|
-
const validateField = useCallback(
|
|
197
|
-
(field: keyof T, value: any): string | undefined => {
|
|
198
|
-
const fieldRules = rules[field];
|
|
199
|
-
if (!fieldRules) return undefined;
|
|
200
|
-
|
|
201
|
-
for (const rule of fieldRules) {
|
|
202
|
-
const error = rule(value);
|
|
203
|
-
if (error) return error;
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
206
|
-
},
|
|
207
|
-
[rules],
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const handleChange = useCallback(
|
|
211
|
-
(field: keyof T) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
212
|
-
const value = e.target.value;
|
|
213
|
-
setValues((prev) => ({ ...prev, [field]: value }));
|
|
214
|
-
|
|
215
|
-
if (touched[field]) {
|
|
216
|
-
const error = validateField(field, value);
|
|
217
|
-
setErrors((prev) => ({ ...prev, [field]: error }));
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
[touched, validateField],
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const handleBlur = useCallback(
|
|
224
|
-
(field: keyof T) => () => {
|
|
225
|
-
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
226
|
-
const error = validateField(field, values[field]);
|
|
227
|
-
setErrors((prev) => ({ ...prev, [field]: error }));
|
|
228
|
-
},
|
|
229
|
-
[values, validateField],
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const validateForm = useCallback((): boolean => {
|
|
233
|
-
const newErrors: Partial<Record<keyof T, string>> = {};
|
|
234
|
-
let isValid = true;
|
|
235
|
-
|
|
236
|
-
Object.keys(rules).forEach((field) => {
|
|
237
|
-
const error = validateField(field as keyof T, values[field as keyof T]);
|
|
238
|
-
if (error) {
|
|
239
|
-
newErrors[field as keyof T] = error;
|
|
240
|
-
isValid = false;
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
setErrors(newErrors);
|
|
245
|
-
setTouched(Object.keys(rules).reduce((acc, field) => ({ ...acc, [field]: true }), {}));
|
|
246
|
-
|
|
247
|
-
return isValid;
|
|
248
|
-
}, [rules, values, validateField]);
|
|
249
|
-
|
|
250
|
-
const reset = useCallback(() => {
|
|
251
|
-
setValues(initialValues);
|
|
252
|
-
setErrors({});
|
|
253
|
-
setTouched({});
|
|
254
|
-
}, [initialValues]);
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
values,
|
|
258
|
-
errors,
|
|
259
|
-
touched,
|
|
260
|
-
handleChange,
|
|
261
|
-
handleBlur,
|
|
262
|
-
validateForm,
|
|
263
|
-
reset,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Common Validation Rules
|
|
269
|
-
|
|
270
|
-
```tsx
|
|
271
|
-
// utils/validationRules.ts
|
|
272
|
-
export const required = (message = "This field is required") => {
|
|
273
|
-
return (value: any) => {
|
|
274
|
-
if (!value || (typeof value === "string" && !value.trim())) {
|
|
275
|
-
return message;
|
|
276
|
-
}
|
|
277
|
-
return undefined;
|
|
278
|
-
};
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
export const email = (message = "Invalid email address") => {
|
|
282
|
-
return (value: string) => {
|
|
283
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
284
|
-
if (value && !emailRegex.test(value)) {
|
|
285
|
-
return message;
|
|
286
|
-
}
|
|
287
|
-
return undefined;
|
|
288
|
-
};
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
export const minLength = (min: number, message?: string) => {
|
|
292
|
-
return (value: string) => {
|
|
293
|
-
if (value && value.length < min) {
|
|
294
|
-
return message || `Must be at least ${min} characters`;
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
};
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
export const maxLength = (max: number, message?: string) => {
|
|
301
|
-
return (value: string) => {
|
|
302
|
-
if (value && value.length > max) {
|
|
303
|
-
return message || `Must be no more than ${max} characters`;
|
|
304
|
-
}
|
|
305
|
-
return undefined;
|
|
306
|
-
};
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
export const pattern = (regex: RegExp, message: string) => {
|
|
310
|
-
return (value: string) => {
|
|
311
|
-
if (value && !regex.test(value)) {
|
|
312
|
-
return message;
|
|
313
|
-
}
|
|
314
|
-
return undefined;
|
|
315
|
-
};
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
export const matchField = (otherField: string, message?: string) => {
|
|
319
|
-
return (value: string, values: any) => {
|
|
320
|
-
if (value && value !== values[otherField]) {
|
|
321
|
-
return message || `Must match ${otherField}`;
|
|
322
|
-
}
|
|
323
|
-
return undefined;
|
|
324
|
-
};
|
|
325
|
-
};
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Using Validation in Forms
|
|
329
|
-
|
|
330
|
-
```tsx
|
|
331
|
-
// components/RegistrationForm.tsx
|
|
332
|
-
"use client";
|
|
333
|
-
|
|
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";
|
|
339
|
-
|
|
340
|
-
export default function RegistrationForm() {
|
|
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
|
-
);
|
|
362
|
-
|
|
363
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
364
|
-
e.preventDefault();
|
|
365
|
-
|
|
366
|
-
if (!validateForm()) {
|
|
367
|
-
toast.error("Please fix validation errors");
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Submit form
|
|
372
|
-
toast.success("Registration successful!");
|
|
373
|
-
reset();
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
378
|
-
<LabelField
|
|
379
|
-
theme="light"
|
|
380
|
-
label="Email"
|
|
381
|
-
requiredLabel="*"
|
|
382
|
-
type="email"
|
|
383
|
-
value={values.email}
|
|
384
|
-
onChange={handleChange("email")}
|
|
385
|
-
onBlur={handleBlur("email")}
|
|
386
|
-
errorMessage={touched.email ? errors.email : undefined}
|
|
387
|
-
/>
|
|
388
|
-
|
|
389
|
-
<LabelField
|
|
390
|
-
theme="light"
|
|
391
|
-
label="Password"
|
|
392
|
-
requiredLabel="*"
|
|
393
|
-
type="password"
|
|
394
|
-
value={values.password}
|
|
395
|
-
onChange={handleChange("password")}
|
|
396
|
-
onBlur={handleBlur("password")}
|
|
397
|
-
errorMessage={touched.password ? errors.password : undefined}
|
|
398
|
-
/>
|
|
399
|
-
|
|
400
|
-
<LabelField
|
|
401
|
-
theme="light"
|
|
402
|
-
label="Confirm Password"
|
|
403
|
-
requiredLabel="*"
|
|
404
|
-
type="password"
|
|
405
|
-
value={values.confirmPassword}
|
|
406
|
-
onChange={handleChange("confirmPassword")}
|
|
407
|
-
onBlur={handleBlur("confirmPassword")}
|
|
408
|
-
errorMessage={touched.confirmPassword ? errors.confirmPassword : undefined}
|
|
409
|
-
/>
|
|
410
|
-
|
|
411
|
-
<Button theme="light" variant="PrimeStyle" type="submit" className="w-full">
|
|
412
|
-
Register
|
|
413
|
-
</Button>
|
|
414
|
-
</form>
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### Async Validation
|
|
420
|
-
|
|
421
|
-
```tsx
|
|
422
|
-
// Example: Check if email is available
|
|
423
|
-
export const emailAvailable = (message = "Email already taken") => {
|
|
424
|
-
return async (value: string) => {
|
|
425
|
-
if (!value) return undefined;
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
const response = await fetch(`/api/check-email?email=${value}`);
|
|
429
|
-
const data = await response.json();
|
|
430
|
-
|
|
431
|
-
if (!data.available) {
|
|
432
|
-
return message;
|
|
433
|
-
}
|
|
434
|
-
} catch (error) {
|
|
435
|
-
console.error("Email validation error:", error);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return undefined;
|
|
439
|
-
};
|
|
440
|
-
};
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Dark Mode Implementation
|
|
446
|
-
|
|
447
|
-
Implement comprehensive dark mode support.
|
|
448
|
-
|
|
449
|
-
### Setup Dark Mode Toggle
|
|
450
|
-
|
|
451
|
-
```tsx
|
|
452
|
-
// components/DarkModeToggle.tsx
|
|
453
|
-
"use client";
|
|
454
|
-
|
|
455
|
-
import { Button } from "@/components/Button";
|
|
456
|
-
import { useTheme } from "@/providers/ThemeProvider";
|
|
457
|
-
import { useEffect, useState } from "react";
|
|
458
|
-
|
|
459
|
-
export default function DarkModeToggle() {
|
|
460
|
-
const { theme, updateTheme } = useTheme();
|
|
461
|
-
const [mounted, setMounted] = useState(false);
|
|
462
|
-
|
|
463
|
-
// Avoid hydration mismatch
|
|
464
|
-
useEffect(() => {
|
|
465
|
-
setMounted(true);
|
|
466
|
-
}, []);
|
|
467
|
-
|
|
468
|
-
if (!mounted) {
|
|
469
|
-
return (
|
|
470
|
-
<Button theme="light" variant="PrimeContStyle" buttonType="icon">
|
|
471
|
-
<div className="w-5 h-5" />
|
|
472
|
-
</Button>
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<Button
|
|
478
|
-
theme={theme as any}
|
|
479
|
-
variant="PrimeContStyle"
|
|
480
|
-
buttonType="icon"
|
|
481
|
-
onClick={() => updateTheme(theme === "light" ? "dark" : "light")}
|
|
482
|
-
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
|
483
|
-
>
|
|
484
|
-
{theme === "light" ? (
|
|
485
|
-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
486
|
-
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
|
487
|
-
</svg>
|
|
488
|
-
) : (
|
|
489
|
-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
490
|
-
<path
|
|
491
|
-
fillRule="evenodd"
|
|
492
|
-
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
|
493
|
-
clipRule="evenodd"
|
|
494
|
-
/>
|
|
495
|
-
</svg>
|
|
496
|
-
)}
|
|
497
|
-
</Button>
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
### Respect System Preferences
|
|
503
|
-
|
|
504
|
-
```tsx
|
|
505
|
-
// hooks/useSystemTheme.ts
|
|
506
|
-
import { useEffect } from "react";
|
|
507
|
-
import { useTheme } from "@/providers/ThemeProvider";
|
|
508
|
-
|
|
509
|
-
export function useSystemTheme() {
|
|
510
|
-
const { theme, updateTheme } = useTheme();
|
|
511
|
-
|
|
512
|
-
useEffect(() => {
|
|
513
|
-
// Only sync with system if theme is 'default'
|
|
514
|
-
if (theme !== "default") return;
|
|
515
|
-
|
|
516
|
-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
517
|
-
|
|
518
|
-
const handleChange = (e: MediaQueryListEvent) => {
|
|
519
|
-
updateTheme(e.matches ? "dark" : "light");
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
// Set initial value
|
|
523
|
-
updateTheme(mediaQuery.matches ? "dark" : "light");
|
|
524
|
-
|
|
525
|
-
// Listen for changes
|
|
526
|
-
mediaQuery.addEventListener("change", handleChange);
|
|
527
|
-
|
|
528
|
-
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
529
|
-
}, [theme, updateTheme]);
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Dark Mode Images
|
|
534
|
-
|
|
535
|
-
```tsx
|
|
536
|
-
// components/ThemedImage.tsx
|
|
537
|
-
import { useTheme } from "@/providers/ThemeProvider";
|
|
538
|
-
import Image from "next/image";
|
|
539
|
-
|
|
540
|
-
interface ThemedImageProps {
|
|
541
|
-
lightSrc: string;
|
|
542
|
-
darkSrc: string;
|
|
543
|
-
alt: string;
|
|
544
|
-
width?: number;
|
|
545
|
-
height?: number;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
export function ThemedImage({
|
|
549
|
-
lightSrc,
|
|
550
|
-
darkSrc,
|
|
551
|
-
alt,
|
|
552
|
-
width = 200,
|
|
553
|
-
height = 200,
|
|
554
|
-
}: ThemedImageProps) {
|
|
555
|
-
const { theme } = useTheme();
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
<Image
|
|
559
|
-
src={theme === "dark" ? darkSrc : lightSrc}
|
|
560
|
-
alt={alt}
|
|
561
|
-
width={width}
|
|
562
|
-
height={height}
|
|
563
|
-
className="transition-opacity duration-200"
|
|
564
|
-
/>
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### Testing Dark Mode
|
|
570
|
-
|
|
571
|
-
```tsx
|
|
572
|
-
// __tests__/DarkMode.test.tsx
|
|
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";
|
|
577
|
-
|
|
578
|
-
describe("Dark Mode", () => {
|
|
579
|
-
it("switches between light and dark themes", async () => {
|
|
580
|
-
const user = userEvent.setup();
|
|
581
|
-
|
|
582
|
-
const { getByRole } = render(
|
|
583
|
-
<ThemeProvider defaultTheme="light">
|
|
584
|
-
<DarkModeToggle />
|
|
585
|
-
</ThemeProvider>,
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
const toggle = getByRole("button");
|
|
589
|
-
|
|
590
|
-
// Initial theme is light
|
|
591
|
-
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
592
|
-
|
|
593
|
-
// Click to switch to dark
|
|
594
|
-
await user.click(toggle);
|
|
595
|
-
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
596
|
-
|
|
597
|
-
// Click to switch back to light
|
|
598
|
-
await user.click(toggle);
|
|
599
|
-
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
---
|
|
605
|
-
|
|
606
|
-
## Accessibility Best Practices
|
|
607
|
-
|
|
608
|
-
Ensure your application is accessible to all users.
|
|
609
|
-
|
|
610
|
-
### Keyboard Navigation
|
|
611
|
-
|
|
612
|
-
```tsx
|
|
613
|
-
// components/AccessibleMenu.tsx
|
|
614
|
-
"use client";
|
|
615
|
-
|
|
616
|
-
import { useState, useRef, useEffect } from "react";
|
|
617
|
-
import { Button } from "@/components/Button";
|
|
618
|
-
|
|
619
|
-
export default function AccessibleMenu() {
|
|
620
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
621
|
-
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
622
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
623
|
-
|
|
624
|
-
const menuItems = ["Profile", "Settings", "Logout"];
|
|
625
|
-
|
|
626
|
-
useEffect(() => {
|
|
627
|
-
if (isOpen) {
|
|
628
|
-
const menuItems = menuRef.current?.querySelectorAll('[role="menuitem"]');
|
|
629
|
-
(menuItems?.[focusedIndex] as HTMLElement)?.focus();
|
|
630
|
-
}
|
|
631
|
-
}, [focusedIndex, isOpen]);
|
|
632
|
-
|
|
633
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
634
|
-
switch (e.key) {
|
|
635
|
-
case "ArrowDown":
|
|
636
|
-
e.preventDefault();
|
|
637
|
-
setFocusedIndex((prev) => (prev + 1) % menuItems.length);
|
|
638
|
-
break;
|
|
639
|
-
case "ArrowUp":
|
|
640
|
-
e.preventDefault();
|
|
641
|
-
setFocusedIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
|
|
642
|
-
break;
|
|
643
|
-
case "Escape":
|
|
644
|
-
setIsOpen(false);
|
|
645
|
-
break;
|
|
646
|
-
case "Home":
|
|
647
|
-
e.preventDefault();
|
|
648
|
-
setFocusedIndex(0);
|
|
649
|
-
break;
|
|
650
|
-
case "End":
|
|
651
|
-
e.preventDefault();
|
|
652
|
-
setFocusedIndex(menuItems.length - 1);
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
return (
|
|
658
|
-
<div className="relative">
|
|
659
|
-
<Button
|
|
660
|
-
theme="light"
|
|
661
|
-
variant="PrimeStyle"
|
|
662
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
663
|
-
aria-expanded={isOpen}
|
|
664
|
-
aria-haspopup="menu"
|
|
665
|
-
>
|
|
666
|
-
Menu
|
|
667
|
-
</Button>
|
|
668
|
-
|
|
669
|
-
{isOpen && (
|
|
670
|
-
<div
|
|
671
|
-
ref={menuRef}
|
|
672
|
-
role="menu"
|
|
673
|
-
onKeyDown={handleKeyDown}
|
|
674
|
-
className="
|
|
675
|
-
absolute top-full mt-2 right-0
|
|
676
|
-
bg-background-presentation-form-base
|
|
677
|
-
border border-border-presentation-global-primary
|
|
678
|
-
rounded-lg shadow-lg
|
|
679
|
-
min-w-[200px]
|
|
680
|
-
"
|
|
681
|
-
>
|
|
682
|
-
{menuItems.map((item, index) => (
|
|
683
|
-
<button
|
|
684
|
-
key={item}
|
|
685
|
-
role="menuitem"
|
|
686
|
-
tabIndex={index === focusedIndex ? 0 : -1}
|
|
687
|
-
onClick={() => {
|
|
688
|
-
console.log(`Clicked ${item}`);
|
|
689
|
-
setIsOpen(false);
|
|
690
|
-
}}
|
|
691
|
-
className="
|
|
692
|
-
w-full px-4 py-2 text-left
|
|
693
|
-
hover:bg-background-presentation-action-hover
|
|
694
|
-
focus:bg-background-presentation-action-hover
|
|
695
|
-
focus:outline-none
|
|
696
|
-
typography-body-medium-regular
|
|
697
|
-
"
|
|
698
|
-
>
|
|
699
|
-
{item}
|
|
700
|
-
</button>
|
|
701
|
-
))}
|
|
702
|
-
</div>
|
|
703
|
-
)}
|
|
704
|
-
</div>
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
### ARIA Labels and Roles
|
|
710
|
-
|
|
711
|
-
```tsx
|
|
712
|
-
// components/AccessibleForm.tsx
|
|
713
|
-
import { Button } from "@/components/Button";
|
|
714
|
-
import { LabelField } from "@/components/LabelField";
|
|
715
|
-
import { useState } from "react";
|
|
716
|
-
|
|
717
|
-
export default function AccessibleForm() {
|
|
718
|
-
const [email, setEmail] = useState("");
|
|
719
|
-
const [error, setError] = useState("");
|
|
720
|
-
|
|
721
|
-
return (
|
|
722
|
-
<form aria-label="Contact form" onSubmit={(e) => e.preventDefault()}>
|
|
723
|
-
<LabelField
|
|
724
|
-
theme="light"
|
|
725
|
-
label="Email"
|
|
726
|
-
requiredLabel="*"
|
|
727
|
-
type="email"
|
|
728
|
-
value={email}
|
|
729
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
730
|
-
aria-invalid={!!error}
|
|
731
|
-
aria-describedby={error ? "email-error" : undefined}
|
|
732
|
-
/>
|
|
733
|
-
|
|
734
|
-
{error && (
|
|
735
|
-
<p
|
|
736
|
-
id="email-error"
|
|
737
|
-
role="alert"
|
|
738
|
-
className="typography-body-small-regular text-content-presentation-state-negative mt-1"
|
|
739
|
-
>
|
|
740
|
-
{error}
|
|
741
|
-
</p>
|
|
742
|
-
)}
|
|
743
|
-
|
|
744
|
-
<Button theme="light" variant="PrimeStyle" type="submit" aria-label="Submit contact form">
|
|
745
|
-
Submit
|
|
746
|
-
</Button>
|
|
747
|
-
</form>
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
### Focus Management
|
|
753
|
-
|
|
754
|
-
```tsx
|
|
755
|
-
// hooks/useFocusTrap.ts
|
|
756
|
-
import { useEffect, useRef } from "react";
|
|
757
|
-
|
|
758
|
-
export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
|
759
|
-
const ref = useRef<T>(null);
|
|
760
|
-
|
|
761
|
-
useEffect(() => {
|
|
762
|
-
if (!isActive) return;
|
|
763
|
-
|
|
764
|
-
const element = ref.current;
|
|
765
|
-
if (!element) return;
|
|
766
|
-
|
|
767
|
-
const focusableElements = element.querySelectorAll(
|
|
768
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
const firstElement = focusableElements[0] as HTMLElement;
|
|
772
|
-
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
773
|
-
|
|
774
|
-
const handleTabKey = (e: KeyboardEvent) => {
|
|
775
|
-
if (e.key !== "Tab") return;
|
|
776
|
-
|
|
777
|
-
if (e.shiftKey && document.activeElement === firstElement) {
|
|
778
|
-
e.preventDefault();
|
|
779
|
-
lastElement?.focus();
|
|
780
|
-
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
781
|
-
e.preventDefault();
|
|
782
|
-
firstElement?.focus();
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
element.addEventListener("keydown", handleTabKey);
|
|
787
|
-
firstElement?.focus();
|
|
788
|
-
|
|
789
|
-
return () => {
|
|
790
|
-
element.removeEventListener("keydown", handleTabKey);
|
|
791
|
-
};
|
|
792
|
-
}, [isActive]);
|
|
793
|
-
|
|
794
|
-
return ref;
|
|
795
|
-
}
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
### Screen Reader Support
|
|
799
|
-
|
|
800
|
-
```tsx
|
|
801
|
-
// components/AccessibleButton.tsx
|
|
802
|
-
import { Button } from "@/components/Button";
|
|
803
|
-
|
|
804
|
-
interface AccessibleButtonProps {
|
|
805
|
-
onClick: () => void;
|
|
806
|
-
isLoading?: boolean;
|
|
807
|
-
children: React.ReactNode;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export function AccessibleButton({ onClick, isLoading, children }: AccessibleButtonProps) {
|
|
811
|
-
return (
|
|
812
|
-
<Button
|
|
813
|
-
theme="light"
|
|
814
|
-
variant="PrimeStyle"
|
|
815
|
-
onClick={onClick}
|
|
816
|
-
disabled={isLoading}
|
|
817
|
-
aria-busy={isLoading}
|
|
818
|
-
aria-label={isLoading ? "Loading..." : undefined}
|
|
819
|
-
>
|
|
820
|
-
{isLoading ? (
|
|
821
|
-
<>
|
|
822
|
-
<span aria-hidden="true">Loading...</span>
|
|
823
|
-
<span className="sr-only">Please wait</span>
|
|
824
|
-
</>
|
|
825
|
-
) : (
|
|
826
|
-
children
|
|
827
|
-
)}
|
|
828
|
-
</Button>
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
---
|
|
834
|
-
|
|
835
|
-
## TypeScript Integration
|
|
836
|
-
|
|
837
|
-
Maximize type safety with TORCH Glare.
|
|
838
|
-
|
|
839
|
-
### Type-Safe Form Handling
|
|
840
|
-
|
|
841
|
-
```tsx
|
|
842
|
-
// types/forms.ts
|
|
843
|
-
export interface ContactForm {
|
|
844
|
-
name: string;
|
|
845
|
-
email: string;
|
|
846
|
-
message: string;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
export type FormErrors<T> = Partial<Record<keyof T, string>>;
|
|
850
|
-
export type FormTouched<T> = Partial<Record<keyof T, boolean>>;
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
```tsx
|
|
854
|
-
// components/TypeSafeForm.tsx
|
|
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";
|
|
860
|
-
|
|
861
|
-
export default function TypeSafeForm() {
|
|
862
|
-
const [formData, setFormData] = useState<ContactForm>({
|
|
863
|
-
name: "",
|
|
864
|
-
email: "",
|
|
865
|
-
message: "",
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
const [errors, setErrors] = useState<FormErrors<ContactForm>>({});
|
|
869
|
-
const [touched, setTouched] = useState<FormTouched<ContactForm>>({});
|
|
870
|
-
|
|
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
|
-
};
|
|
879
|
-
|
|
880
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
881
|
-
e.preventDefault();
|
|
882
|
-
toast.success("Form submitted!");
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
return (
|
|
886
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
887
|
-
<LabelField
|
|
888
|
-
theme="light"
|
|
889
|
-
label="Name"
|
|
890
|
-
value={formData.name}
|
|
891
|
-
onChange={handleChange("name")}
|
|
892
|
-
errorMessage={touched.name ? errors.name : undefined}
|
|
893
|
-
/>
|
|
894
|
-
|
|
895
|
-
{/* More fields... */}
|
|
896
|
-
|
|
897
|
-
<Button theme="light" variant="PrimeStyle" type="submit">
|
|
898
|
-
Submit
|
|
899
|
-
</Button>
|
|
900
|
-
</form>
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
### Generic Components
|
|
906
|
-
|
|
907
|
-
```tsx
|
|
908
|
-
// components/TypedList.tsx
|
|
909
|
-
interface ListProps<T> {
|
|
910
|
-
items: T[];
|
|
911
|
-
renderItem: (item: T) => React.ReactNode;
|
|
912
|
-
keyExtractor: (item: T) => string;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
export function TypedList<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
|
916
|
-
return (
|
|
917
|
-
<div className="space-y-2">
|
|
918
|
-
{items.map((item) => (
|
|
919
|
-
<div key={keyExtractor(item)}>{renderItem(item)}</div>
|
|
920
|
-
))}
|
|
921
|
-
</div>
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
### Type-Safe Hooks
|
|
927
|
-
|
|
928
|
-
```tsx
|
|
929
|
-
// hooks/useTypedLocalStorage.ts
|
|
930
|
-
import { useState, useEffect } from "react";
|
|
931
|
-
|
|
932
|
-
export function useTypedLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
933
|
-
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
934
|
-
if (typeof window === "undefined") return initialValue;
|
|
935
|
-
|
|
936
|
-
try {
|
|
937
|
-
const item = window.localStorage.getItem(key);
|
|
938
|
-
return item ? (JSON.parse(item) as T) : initialValue;
|
|
939
|
-
} catch (error) {
|
|
940
|
-
console.error(`Error loading ${key} from localStorage:`, error);
|
|
941
|
-
return initialValue;
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
const setValue = (value: T) => {
|
|
946
|
-
try {
|
|
947
|
-
setStoredValue(value);
|
|
948
|
-
if (typeof window !== "undefined") {
|
|
949
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
950
|
-
}
|
|
951
|
-
} catch (error) {
|
|
952
|
-
console.error(`Error saving ${key} to localStorage:`, error);
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
return [storedValue, setValue];
|
|
957
|
-
}
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
### Extending Component Types
|
|
961
|
-
|
|
962
|
-
```tsx
|
|
963
|
-
// types/components.ts
|
|
964
|
-
import type { ButtonProps } from "@/components/LoginButton";
|
|
965
|
-
|
|
966
|
-
export interface ExtendedButtonProps extends ButtonProps {
|
|
967
|
-
isLoading?: boolean;
|
|
968
|
-
loadingText?: string;
|
|
969
|
-
}
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
```tsx
|
|
973
|
-
// components/ExtendedButton.tsx
|
|
974
|
-
import { Button } from "@/components/Button";
|
|
975
|
-
import type { ExtendedButtonProps } from "../types/components";
|
|
976
|
-
|
|
977
|
-
export function ExtendedButton({
|
|
978
|
-
isLoading,
|
|
979
|
-
loadingText = "Loading...",
|
|
980
|
-
children,
|
|
981
|
-
...props
|
|
982
|
-
}: ExtendedButtonProps) {
|
|
983
|
-
return (
|
|
984
|
-
<Button {...props} disabled={props.disabled || isLoading}>
|
|
985
|
-
{isLoading ? loadingText : children}
|
|
986
|
-
</Button>
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
### Type Guards
|
|
992
|
-
|
|
993
|
-
```tsx
|
|
994
|
-
// utils/typeGuards.ts
|
|
995
|
-
import type { User } from "../types";
|
|
996
|
-
|
|
997
|
-
export function isValidEmail(value: unknown): value is string {
|
|
998
|
-
return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
export function isUser(value: unknown): value is User {
|
|
1002
|
-
return (
|
|
1003
|
-
typeof value === "object" &&
|
|
1004
|
-
value !== null &&
|
|
1005
|
-
"id" in value &&
|
|
1006
|
-
"email" in value &&
|
|
1007
|
-
"name" in value
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
```
|
|
1011
|
-
|
|
1012
|
-
---
|
|
1013
|
-
|
|
1014
|
-
## Additional Resources
|
|
1015
|
-
|
|
1016
|
-
- [Tailwind CSS Documentation](https://tailwindcss.com)
|
|
1017
|
-
- [React Accessibility](https://reactjs.org/docs/accessibility.html)
|
|
1018
|
-
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|
|
1019
|
-
- [WCAG Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
1020
|
-
- [MDN Web Docs](https://developer.mozilla.org)
|
|
1021
|
-
|
|
1022
|
-
## Need More Help?
|
|
1023
|
-
|
|
1024
|
-
- [Component Documentation](../components/)
|
|
1025
|
-
- [Tutorials](../tutorials/)
|
|
1026
|
-
- [API Reference](../reference/)
|
|
1027
|
-
- [GitHub Issues](https://github.com/torch-ai/torch-glare/issues)
|