torch-glare 2.5.5 → 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 -338
- package/apps/lib/components/Breadcrumb.tsx +0 -280
- package/apps/lib/components/Button.tsx +0 -317
- 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 -383
- 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 -84
- 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 -63
- package/apps/lib/components/DataViews/states.tsx +0 -38
- 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 -420
- package/apps/lib/components/DropdownMenu.tsx +0 -540
- package/apps/lib/components/FieldHint.tsx +0 -69
- package/apps/lib/components/Form.tsx +0 -182
- package/apps/lib/components/FormBuilder/context.ts +0 -73
- 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 -163
- 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 -91
- 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 -47
- package/apps/lib/components/FormBuilder/types.ts +0 -311
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -128
- package/apps/lib/components/FormRenderer/detail.tsx +0 -207
- package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -297
- package/apps/lib/components/FormRenderer/header.tsx +0 -88
- package/apps/lib/components/FormRenderer/index.ts +0 -15
- package/apps/lib/components/FormRenderer/section.tsx +0 -39
- package/apps/lib/components/FormRenderer/stepper.tsx +0 -339
- package/apps/lib/components/FormRenderer/types.ts +0 -78
- 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 -147
- 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 -253
- 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 -610
- 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 -58
- package/apps/lib/hooks/useClickOutside.ts +0 -27
- package/apps/lib/hooks/useDragDrop.tsx +0 -365
- 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 -214
- 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 -1201
- 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/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 -712
- 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 -1495
- package/docs/components/data-views/migration.md +0 -79
- 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 -276
- package/docs/components/form-renderer.md +0 -326
- 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 -477
- 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 -54
- 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 -1500
- 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 -704
- 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
|
@@ -1,906 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: ImageAttachment
|
|
3
|
-
description: File upload component suite with drag-and-drop, preview thumbnails, and expandable modal view
|
|
4
|
-
group: Advanced Components
|
|
5
|
-
keywords: [image-attachment, file-upload, drag-drop, preview, modal, expandable]
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# ImageAttachment
|
|
9
|
-
|
|
10
|
-
> A comprehensive file upload component suite featuring a drag-and-drop zone (ImageAttachment), thumbnail preview with expand capability (ExpandableImage), and full-size modal preview (AttachmentImagePreview). Perfect for building file upload interfaces with visual feedback.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
TORCH Glare is a copy-in library: the CLI copies this component's source into your project
|
|
15
|
-
(you do **not** install it from the npm package). Run `init` once, then `add`:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npx torch-glare@latest init
|
|
19
|
-
npx torch-glare@latest add ImageAttachment
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`add` also copies any components, hooks, and utilities that `ImageAttachment` depends on.
|
|
23
|
-
|
|
24
|
-
## Import
|
|
25
|
-
|
|
26
|
-
Import from your project's local path — the alias configured in `glare.json` (e.g. `@/*`):
|
|
27
|
-
|
|
28
|
-
```tsx
|
|
29
|
-
import { ImageAttachment } from "@/components/ImageAttachment";
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Component Overview
|
|
33
|
-
|
|
34
|
-
The ImageAttachment suite consists of three components:
|
|
35
|
-
|
|
36
|
-
1. **ImageAttachment**: Drop zone and file input for uploading
|
|
37
|
-
2. **ExpandableImage**: Thumbnail preview with click-to-expand
|
|
38
|
-
3. **AttachmentImagePreview**: Full-size modal view with actions
|
|
39
|
-
|
|
40
|
-
## Quick Examples
|
|
41
|
-
|
|
42
|
-
### Basic File Upload
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { ImageAttachment } from "@/components/ImageAttachment";
|
|
46
|
-
import { useState } from 'react'
|
|
47
|
-
|
|
48
|
-
function Example() {
|
|
49
|
-
const [file, setFile] = useState<File | null>(null)
|
|
50
|
-
|
|
51
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
52
|
-
const selectedFile = e.target.files?.[0]
|
|
53
|
-
if (selectedFile) {
|
|
54
|
-
setFile(selectedFile)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<ImageAttachment
|
|
60
|
-
mainLabel="Drop your image here"
|
|
61
|
-
secondaryLabel="or click to browse"
|
|
62
|
-
expandLabel="View"
|
|
63
|
-
onChange={handleChange}
|
|
64
|
-
accept="image/*"
|
|
65
|
-
/>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### With Preview
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { ImageAttachment, ExpandableImage } from "@/components/ImageAttachment";
|
|
74
|
-
import { useState } from 'react'
|
|
75
|
-
|
|
76
|
-
function WithPreview() {
|
|
77
|
-
const [preview, setPreview] = useState<string>('')
|
|
78
|
-
|
|
79
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
80
|
-
const file = e.target.files?.[0]
|
|
81
|
-
if (file) {
|
|
82
|
-
const reader = new FileReader()
|
|
83
|
-
reader.onloadend = () => {
|
|
84
|
-
setPreview(reader.result as string)
|
|
85
|
-
}
|
|
86
|
-
reader.readAsDataURL(file)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div className="flex gap-4">
|
|
92
|
-
<ImageAttachment
|
|
93
|
-
mainLabel="Upload Image"
|
|
94
|
-
secondaryLabel="PNG, JPG up to 5MB"
|
|
95
|
-
expandLabel="View"
|
|
96
|
-
onChange={handleChange}
|
|
97
|
-
accept="image/*"
|
|
98
|
-
/>
|
|
99
|
-
|
|
100
|
-
{preview && (
|
|
101
|
-
<ExpandableImage
|
|
102
|
-
previewSrc={preview}
|
|
103
|
-
expandLabel="Expand"
|
|
104
|
-
placeholderLabel="Upload Image"
|
|
105
|
-
/>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Complete Upload Flow
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { ImageAttachment, ExpandableImage, AttachmentImagePreview } from "@/components/ImageAttachment";
|
|
116
|
-
import { Button } from "@/components/Button";
|
|
117
|
-
import { useState } from 'react'
|
|
118
|
-
|
|
119
|
-
function CompleteFlow() {
|
|
120
|
-
const [preview, setPreview] = useState<string>('')
|
|
121
|
-
const [file, setFile] = useState<File | null>(null)
|
|
122
|
-
|
|
123
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
124
|
-
const selectedFile = e.target.files?.[0]
|
|
125
|
-
if (selectedFile) {
|
|
126
|
-
setFile(selectedFile)
|
|
127
|
-
const reader = new FileReader()
|
|
128
|
-
reader.onloadend = () => setPreview(reader.result as string)
|
|
129
|
-
reader.readAsDataURL(selectedFile)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const handleUpload = () => {
|
|
134
|
-
if (file) {
|
|
135
|
-
// Upload logic here
|
|
136
|
-
console.log('Uploading:', file.name)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const handleRemove = () => {
|
|
141
|
-
setPreview('')
|
|
142
|
-
setFile(null)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<div className="flex gap-4">
|
|
147
|
-
<ImageAttachment
|
|
148
|
-
mainLabel="Drop image here"
|
|
149
|
-
secondaryLabel="or click to browse"
|
|
150
|
-
expandLabel="View"
|
|
151
|
-
onChange={handleChange}
|
|
152
|
-
accept="image/*"
|
|
153
|
-
/>
|
|
154
|
-
|
|
155
|
-
{preview && (
|
|
156
|
-
<ExpandableImage
|
|
157
|
-
previewSrc={preview}
|
|
158
|
-
expandLabel="Expand"
|
|
159
|
-
>
|
|
160
|
-
<AttachmentImagePreview
|
|
161
|
-
src={preview}
|
|
162
|
-
header={file?.name || 'Image Preview'}
|
|
163
|
-
>
|
|
164
|
-
<Button onClick={handleUpload}>Upload</Button>
|
|
165
|
-
<Button variant="BluSecStyle" onClick={handleRemove}>
|
|
166
|
-
Remove
|
|
167
|
-
</Button>
|
|
168
|
-
</AttachmentImagePreview>
|
|
169
|
-
</ExpandableImage>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### With Drag and Drop (React Dropzone)
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
import { ImageAttachment, ExpandableImage } from "@/components/ImageAttachment";
|
|
180
|
-
import { useDropzone } from 'react-dropzone'
|
|
181
|
-
import { useState } from 'react'
|
|
182
|
-
|
|
183
|
-
function WithDropzone() {
|
|
184
|
-
const [preview, setPreview] = useState<string>('')
|
|
185
|
-
|
|
186
|
-
const onDrop = (acceptedFiles: File[]) => {
|
|
187
|
-
const file = acceptedFiles[0]
|
|
188
|
-
if (file) {
|
|
189
|
-
const reader = new FileReader()
|
|
190
|
-
reader.onloadend = () => setPreview(reader.result as string)
|
|
191
|
-
reader.readAsDataURL(file)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
196
|
-
onDrop,
|
|
197
|
-
accept: { 'image/*': [] },
|
|
198
|
-
multiple: false,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
<div className="flex gap-4">
|
|
203
|
-
<ImageAttachment
|
|
204
|
-
mainLabel="Drop your image here"
|
|
205
|
-
secondaryLabel="PNG, JPG, GIF up to 10MB"
|
|
206
|
-
expandLabel="View"
|
|
207
|
-
getRootProps={getRootProps}
|
|
208
|
-
isDropAreaActive={isDragActive}
|
|
209
|
-
{...getInputProps()}
|
|
210
|
-
/>
|
|
211
|
-
|
|
212
|
-
{preview && (
|
|
213
|
-
<ExpandableImage
|
|
214
|
-
previewSrc={preview}
|
|
215
|
-
expandLabel="Expand"
|
|
216
|
-
/>
|
|
217
|
-
)}
|
|
218
|
-
</div>
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Multiple File Upload
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { ImageAttachment, ExpandableImage } from "@/components/ImageAttachment";
|
|
227
|
-
import { useState } from 'react'
|
|
228
|
-
|
|
229
|
-
function MultipleFiles() {
|
|
230
|
-
const [previews, setPreviews] = useState<string[]>([])
|
|
231
|
-
|
|
232
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
233
|
-
const files = Array.from(e.target.files || [])
|
|
234
|
-
|
|
235
|
-
files.forEach(file => {
|
|
236
|
-
const reader = new FileReader()
|
|
237
|
-
reader.onloadend = () => {
|
|
238
|
-
setPreviews(prev => [...prev, reader.result as string])
|
|
239
|
-
}
|
|
240
|
-
reader.readAsDataURL(file)
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return (
|
|
245
|
-
<div className="space-y-4">
|
|
246
|
-
<ImageAttachment
|
|
247
|
-
mainLabel="Upload Images"
|
|
248
|
-
secondaryLabel="Select multiple files"
|
|
249
|
-
expandLabel="View"
|
|
250
|
-
onChange={handleChange}
|
|
251
|
-
accept="image/*"
|
|
252
|
-
multiple
|
|
253
|
-
/>
|
|
254
|
-
|
|
255
|
-
<div className="flex gap-2 flex-wrap">
|
|
256
|
-
{previews.map((preview, index) => (
|
|
257
|
-
<ExpandableImage
|
|
258
|
-
key={index}
|
|
259
|
-
previewSrc={preview}
|
|
260
|
-
expandLabel="Expand"
|
|
261
|
-
/>
|
|
262
|
-
))}
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### With Validation
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
import { ImageAttachment } from "@/components/ImageAttachment";
|
|
273
|
-
import { FieldHint } from "@/components/FieldHint";
|
|
274
|
-
import { toast } from "@/components/Toast";
|
|
275
|
-
import { useState } from 'react'
|
|
276
|
-
|
|
277
|
-
function WithValidation() {
|
|
278
|
-
const [error, setError] = useState('')
|
|
279
|
-
|
|
280
|
-
const validateFile = (file: File) => {
|
|
281
|
-
const maxSize = 5 * 1024 * 1024 // 5MB
|
|
282
|
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
|
283
|
-
|
|
284
|
-
if (!allowedTypes.includes(file.type)) {
|
|
285
|
-
return 'Only JPEG, PNG, and GIF files are allowed'
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (file.size > maxSize) {
|
|
289
|
-
return 'File size must be less than 5MB'
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return null
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
296
|
-
const file = e.target.files?.[0]
|
|
297
|
-
if (file) {
|
|
298
|
-
const validationError = validateFile(file)
|
|
299
|
-
|
|
300
|
-
if (validationError) {
|
|
301
|
-
setError(validationError)
|
|
302
|
-
toast.error(validationError)
|
|
303
|
-
e.target.value = '' // Clear input
|
|
304
|
-
} else {
|
|
305
|
-
setError('')
|
|
306
|
-
// Process file
|
|
307
|
-
console.log('Valid file:', file.name)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return (
|
|
313
|
-
<div className="space-y-2">
|
|
314
|
-
<ImageAttachment
|
|
315
|
-
mainLabel="Upload Image"
|
|
316
|
-
secondaryLabel="PNG, JPG, GIF (max 5MB)"
|
|
317
|
-
expandLabel="View"
|
|
318
|
-
onChange={handleChange}
|
|
319
|
-
accept="image/jpeg,image/png,image/gif"
|
|
320
|
-
/>
|
|
321
|
-
|
|
322
|
-
{error && (
|
|
323
|
-
<FieldHint state="error" label={error} />
|
|
324
|
-
)}
|
|
325
|
-
</div>
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### Profile Picture Upload
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import { ImageAttachment, ExpandableImage, AttachmentImagePreview } from "@/components/ImageAttachment";
|
|
334
|
-
import { Avatar } from "@/components/Avatar";
|
|
335
|
-
import { Button } from "@/components/Button";
|
|
336
|
-
import { useState } from 'react'
|
|
337
|
-
|
|
338
|
-
function ProfilePictureUpload() {
|
|
339
|
-
const [preview, setPreview] = useState<string>('')
|
|
340
|
-
const [uploading, setUploading] = useState(false)
|
|
341
|
-
|
|
342
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
343
|
-
const file = e.target.files?.[0]
|
|
344
|
-
if (file) {
|
|
345
|
-
const reader = new FileReader()
|
|
346
|
-
reader.onloadend = () => setPreview(reader.result as string)
|
|
347
|
-
reader.readAsDataURL(file)
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const handleUpload = async () => {
|
|
352
|
-
setUploading(true)
|
|
353
|
-
// Simulate upload
|
|
354
|
-
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
355
|
-
setUploading(false)
|
|
356
|
-
toast.success('Profile picture updated!')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return (
|
|
360
|
-
<div className="flex items-center gap-4">
|
|
361
|
-
<Avatar
|
|
362
|
-
src={preview || '/default-avatar.png'}
|
|
363
|
-
alt="Profile"
|
|
364
|
-
size="L"
|
|
365
|
-
/>
|
|
366
|
-
|
|
367
|
-
<div className="flex gap-4">
|
|
368
|
-
<ImageAttachment
|
|
369
|
-
mainLabel="Change Photo"
|
|
370
|
-
secondaryLabel="Click to upload"
|
|
371
|
-
expandLabel="View"
|
|
372
|
-
onChange={handleChange}
|
|
373
|
-
accept="image/*"
|
|
374
|
-
/>
|
|
375
|
-
|
|
376
|
-
{preview && (
|
|
377
|
-
<ExpandableImage
|
|
378
|
-
previewSrc={preview}
|
|
379
|
-
expandLabel="Preview"
|
|
380
|
-
>
|
|
381
|
-
<AttachmentImagePreview
|
|
382
|
-
src={preview}
|
|
383
|
-
header="Profile Picture"
|
|
384
|
-
>
|
|
385
|
-
<Button
|
|
386
|
-
onClick={handleUpload}
|
|
387
|
-
disabled={uploading}
|
|
388
|
-
>
|
|
389
|
-
{uploading ? 'Uploading...' : 'Save'}
|
|
390
|
-
</Button>
|
|
391
|
-
</AttachmentImagePreview>
|
|
392
|
-
</ExpandableImage>
|
|
393
|
-
)}
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Gallery Upload
|
|
401
|
-
|
|
402
|
-
```typescript
|
|
403
|
-
import { ImageAttachment, ExpandableImage } from "@/components/ImageAttachment";
|
|
404
|
-
import { Button } from "@/components/Button";
|
|
405
|
-
import { useState } from 'react'
|
|
406
|
-
|
|
407
|
-
function GalleryUpload() {
|
|
408
|
-
const [images, setImages] = useState<Array<{ id: string; url: string }>>([])
|
|
409
|
-
|
|
410
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
411
|
-
const files = Array.from(e.target.files || [])
|
|
412
|
-
|
|
413
|
-
files.forEach(file => {
|
|
414
|
-
const reader = new FileReader()
|
|
415
|
-
reader.onloadend = () => {
|
|
416
|
-
setImages(prev => [
|
|
417
|
-
...prev,
|
|
418
|
-
{ id: Math.random().toString(), url: reader.result as string }
|
|
419
|
-
])
|
|
420
|
-
}
|
|
421
|
-
reader.readAsDataURL(file)
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const handleRemove = (id: string) => {
|
|
426
|
-
setImages(images.filter(img => img.id !== id))
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<div className="space-y-4">
|
|
431
|
-
<ImageAttachment
|
|
432
|
-
mainLabel="Add Photos"
|
|
433
|
-
secondaryLabel="Upload up to 10 images"
|
|
434
|
-
expandLabel="View"
|
|
435
|
-
onChange={handleChange}
|
|
436
|
-
accept="image/*"
|
|
437
|
-
multiple
|
|
438
|
-
/>
|
|
439
|
-
|
|
440
|
-
{images.length > 0 && (
|
|
441
|
-
<div className="grid grid-cols-4 gap-4">
|
|
442
|
-
{images.map(image => (
|
|
443
|
-
<div key={image.id} className="relative group">
|
|
444
|
-
<ExpandableImage
|
|
445
|
-
previewSrc={image.url}
|
|
446
|
-
expandLabel="Expand"
|
|
447
|
-
/>
|
|
448
|
-
<Button
|
|
449
|
-
size="S"
|
|
450
|
-
buttonType="icon"
|
|
451
|
-
variant="BluSecStyle"
|
|
452
|
-
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100"
|
|
453
|
-
onClick={() => handleRemove(image.id)}
|
|
454
|
-
>
|
|
455
|
-
<i className="ri-close-line" />
|
|
456
|
-
</Button>
|
|
457
|
-
</div>
|
|
458
|
-
))}
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
461
|
-
</div>
|
|
462
|
-
)
|
|
463
|
-
}
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
## API Reference
|
|
467
|
-
|
|
468
|
-
### ImageAttachment Props
|
|
469
|
-
|
|
470
|
-
Extends all input HTML attributes.
|
|
471
|
-
|
|
472
|
-
| Prop | Type | Default | Description |
|
|
473
|
-
|------|------|---------|-------------|
|
|
474
|
-
| `mainLabel` | `string` | **required** | Primary label text |
|
|
475
|
-
| `secondaryLabel` | `string` | **required** | Secondary helper text |
|
|
476
|
-
| `expandLabel` | `ReactNode` | **required** | Label for expand action |
|
|
477
|
-
| `isDropAreaActive` | `boolean` | `false` | Active drag-over state |
|
|
478
|
-
| `theme` | `'light' \| 'dark' \| 'default'` | - | Theme variant |
|
|
479
|
-
| `getRootProps` | `() => any` | - | React Dropzone root props |
|
|
480
|
-
| `children` | `ReactNode` | - | Additional content |
|
|
481
|
-
| `className` | `string` | - | Additional CSS classes |
|
|
482
|
-
| ...InputHTMLAttributes | - | - | All input attributes |
|
|
483
|
-
|
|
484
|
-
### ExpandableImage Props
|
|
485
|
-
|
|
486
|
-
| Prop | Type | Default | Description |
|
|
487
|
-
|------|------|---------|-------------|
|
|
488
|
-
| `previewSrc` | `string` | **required** | Image source URL or data URL |
|
|
489
|
-
| `expandLabel` | `ReactNode` | **required** | Label shown on hover |
|
|
490
|
-
| `placeholderLabel` | `string` | `'Upload Image'` | Placeholder text when no image |
|
|
491
|
-
| `theme` | `'light' \| 'dark' \| 'default'` | - | Theme variant |
|
|
492
|
-
| `className` | `string` | - | Additional CSS classes |
|
|
493
|
-
| `children` | `ReactNode` | - | Modal content (AttachmentImagePreview) |
|
|
494
|
-
| ...HTMLDivAttributes | - | - | All div attributes |
|
|
495
|
-
|
|
496
|
-
### AttachmentImagePreview Props
|
|
497
|
-
|
|
498
|
-
| Prop | Type | Default | Description |
|
|
499
|
-
|------|------|---------|-------------|
|
|
500
|
-
| `src` | `string` | **required** | Full-size image source |
|
|
501
|
-
| `header` | `ReactNode` | - | Modal header text |
|
|
502
|
-
| `theme` | `'light' \| 'dark' \| 'default'` | - | Theme variant |
|
|
503
|
-
| `children` | `ReactNode` | - | Action buttons |
|
|
504
|
-
| `className` | `string` | - | Additional CSS classes |
|
|
505
|
-
| ...HTMLDivAttributes | - | - | All div attributes |
|
|
506
|
-
|
|
507
|
-
## TypeScript
|
|
508
|
-
|
|
509
|
-
### Full Type Definitions
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
import { InputHTMLAttributes, HTMLAttributes, ReactNode } from 'react'
|
|
513
|
-
|
|
514
|
-
interface ImageAttachmentProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
515
|
-
isDropAreaActive?: boolean
|
|
516
|
-
mainLabel: string
|
|
517
|
-
secondaryLabel: string
|
|
518
|
-
theme?: 'light' | 'dark' | 'default'
|
|
519
|
-
expandLabel: ReactNode
|
|
520
|
-
children?: ReactNode
|
|
521
|
-
getRootProps?: () => any
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
interface ExpandableImageProps extends HTMLAttributes<HTMLDivElement> {
|
|
525
|
-
previewSrc: string
|
|
526
|
-
expandLabel: ReactNode
|
|
527
|
-
placeholderLabel?: string
|
|
528
|
-
theme?: 'light' | 'dark' | 'default'
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
interface AttachmentImagePreviewProps extends HTMLAttributes<HTMLDivElement> {
|
|
532
|
-
src: string
|
|
533
|
-
header?: ReactNode
|
|
534
|
-
theme?: 'light' | 'dark' | 'default'
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export const ImageAttachment: React.ForwardRefExoticComponent<
|
|
538
|
-
ImageAttachmentProps & React.RefAttributes<HTMLInputElement>
|
|
539
|
-
>
|
|
540
|
-
|
|
541
|
-
export const ExpandableImage: React.FC<ExpandableImageProps>
|
|
542
|
-
export const AttachmentImagePreview: React.FC<AttachmentImagePreviewProps>
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### Usage with Types
|
|
546
|
-
|
|
547
|
-
```typescript
|
|
548
|
-
import { ImageAttachment, ExpandableImage, AttachmentImagePreview } from "@/components/ImageAttachment";
|
|
549
|
-
import { useRef, useState } from 'react'
|
|
550
|
-
|
|
551
|
-
function TypedExample() {
|
|
552
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
553
|
-
const [preview, setPreview] = useState<string | null>(null)
|
|
554
|
-
const [file, setFile] = useState<File | null>(null)
|
|
555
|
-
|
|
556
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
557
|
-
const selectedFile = e.target.files?.[0]
|
|
558
|
-
if (selectedFile) {
|
|
559
|
-
setFile(selectedFile)
|
|
560
|
-
const reader = new FileReader()
|
|
561
|
-
reader.onloadend = () => setPreview(reader.result as string)
|
|
562
|
-
reader.readAsDataURL(selectedFile)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return (
|
|
567
|
-
<ImageAttachment
|
|
568
|
-
ref={inputRef}
|
|
569
|
-
mainLabel="Upload"
|
|
570
|
-
secondaryLabel="PNG or JPG"
|
|
571
|
-
expandLabel="View"
|
|
572
|
-
onChange={handleChange}
|
|
573
|
-
accept="image/png,image/jpeg"
|
|
574
|
-
/>
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
## Common Patterns
|
|
580
|
-
|
|
581
|
-
### Upload with Progress
|
|
582
|
-
|
|
583
|
-
```typescript
|
|
584
|
-
import { ImageAttachment } from "@/components/ImageAttachment";
|
|
585
|
-
import { useState } from 'react'
|
|
586
|
-
|
|
587
|
-
function UploadWithProgress() {
|
|
588
|
-
const [uploading, setUploading] = useState(false)
|
|
589
|
-
const [progress, setProgress] = useState(0)
|
|
590
|
-
|
|
591
|
-
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
592
|
-
const file = e.target.files?.[0]
|
|
593
|
-
if (!file) return
|
|
594
|
-
|
|
595
|
-
setUploading(true)
|
|
596
|
-
setProgress(0)
|
|
597
|
-
|
|
598
|
-
// Simulate upload progress
|
|
599
|
-
const interval = setInterval(() => {
|
|
600
|
-
setProgress(prev => {
|
|
601
|
-
if (prev >= 100) {
|
|
602
|
-
clearInterval(interval)
|
|
603
|
-
setUploading(false)
|
|
604
|
-
return 100
|
|
605
|
-
}
|
|
606
|
-
return prev + 10
|
|
607
|
-
})
|
|
608
|
-
}, 200)
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return (
|
|
612
|
-
<div className="space-y-2">
|
|
613
|
-
<ImageAttachment
|
|
614
|
-
mainLabel="Upload Image"
|
|
615
|
-
secondaryLabel="PNG, JPG (max 5MB)"
|
|
616
|
-
expandLabel="View"
|
|
617
|
-
onChange={handleChange}
|
|
618
|
-
disabled={uploading}
|
|
619
|
-
/>
|
|
620
|
-
|
|
621
|
-
{uploading && (
|
|
622
|
-
<div className="w-full bg-gray-200 rounded">
|
|
623
|
-
<div
|
|
624
|
-
className="bg-blue-600 h-2 rounded transition-all"
|
|
625
|
-
style={{ width: `${progress}%` }}
|
|
626
|
-
/>
|
|
627
|
-
</div>
|
|
628
|
-
)}
|
|
629
|
-
</div>
|
|
630
|
-
)
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### Image Cropper Integration
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
import { ImageAttachment, ExpandableImage } from "@/components/ImageAttachment";
|
|
638
|
-
import { useState } from 'react'
|
|
639
|
-
import Cropper from 'react-easy-crop'
|
|
640
|
-
|
|
641
|
-
function ImageCropper() {
|
|
642
|
-
const [image, setImage] = useState<string>('')
|
|
643
|
-
const [crop, setCrop] = useState({ x: 0, y: 0 })
|
|
644
|
-
const [zoom, setZoom] = useState(1)
|
|
645
|
-
|
|
646
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
647
|
-
const file = e.target.files?.[0]
|
|
648
|
-
if (file) {
|
|
649
|
-
const reader = new FileReader()
|
|
650
|
-
reader.onloadend = () => setImage(reader.result as string)
|
|
651
|
-
reader.readAsDataURL(file)
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return (
|
|
656
|
-
<div className="space-y-4">
|
|
657
|
-
<ImageAttachment
|
|
658
|
-
mainLabel="Upload to Crop"
|
|
659
|
-
secondaryLabel="Select an image"
|
|
660
|
-
expandLabel="Crop"
|
|
661
|
-
onChange={handleChange}
|
|
662
|
-
/>
|
|
663
|
-
|
|
664
|
-
{image && (
|
|
665
|
-
<div className="relative h-64">
|
|
666
|
-
<Cropper
|
|
667
|
-
image={image}
|
|
668
|
-
crop={crop}
|
|
669
|
-
zoom={zoom}
|
|
670
|
-
aspect={1}
|
|
671
|
-
onCropChange={setCrop}
|
|
672
|
-
onZoomChange={setZoom}
|
|
673
|
-
/>
|
|
674
|
-
</div>
|
|
675
|
-
)}
|
|
676
|
-
</div>
|
|
677
|
-
)
|
|
678
|
-
}
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
## Testing
|
|
682
|
-
|
|
683
|
-
### Unit Test Examples
|
|
684
|
-
|
|
685
|
-
```typescript
|
|
686
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
687
|
-
import { ImageAttachment, ExpandableImage, AttachmentImagePreview } from "@/components/ImageAttachment";
|
|
688
|
-
|
|
689
|
-
describe('ImageAttachment', () => {
|
|
690
|
-
it('renders labels', () => {
|
|
691
|
-
render(
|
|
692
|
-
<ImageAttachment
|
|
693
|
-
mainLabel="Upload"
|
|
694
|
-
secondaryLabel="Click to browse"
|
|
695
|
-
expandLabel="View"
|
|
696
|
-
/>
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
expect(screen.getByText('Upload')).toBeInTheDocument()
|
|
700
|
-
expect(screen.getByText('Click to browse')).toBeInTheDocument()
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
it('handles file selection', async () => {
|
|
704
|
-
const handleChange = jest.fn()
|
|
705
|
-
|
|
706
|
-
render(
|
|
707
|
-
<ImageAttachment
|
|
708
|
-
mainLabel="Upload"
|
|
709
|
-
secondaryLabel="Browse"
|
|
710
|
-
expandLabel="View"
|
|
711
|
-
onChange={handleChange}
|
|
712
|
-
/>
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
const input = screen.getByRole('textbox', { hidden: true }) as HTMLInputElement
|
|
716
|
-
const file = new File(['test'], 'test.png', { type: 'image/png' })
|
|
717
|
-
|
|
718
|
-
fireEvent.change(input, { target: { files: [file] } })
|
|
719
|
-
|
|
720
|
-
expect(handleChange).toHaveBeenCalled()
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
it('shows active state when dragging', () => {
|
|
724
|
-
const { container } = render(
|
|
725
|
-
<ImageAttachment
|
|
726
|
-
mainLabel="Upload"
|
|
727
|
-
secondaryLabel="Drop here"
|
|
728
|
-
expandLabel="View"
|
|
729
|
-
isDropAreaActive
|
|
730
|
-
/>
|
|
731
|
-
)
|
|
732
|
-
|
|
733
|
-
// Check for active styling
|
|
734
|
-
expect(container.querySelector('.bg-background-presentation-action-hovercontstyle')).toBeInTheDocument()
|
|
735
|
-
})
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
describe('ExpandableImage', () => {
|
|
739
|
-
it('renders placeholder when no image', () => {
|
|
740
|
-
render(
|
|
741
|
-
<ExpandableImage
|
|
742
|
-
previewSrc=""
|
|
743
|
-
expandLabel="Expand"
|
|
744
|
-
placeholderLabel="No Image"
|
|
745
|
-
/>
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
expect(screen.getByText('No Image')).toBeInTheDocument()
|
|
749
|
-
})
|
|
750
|
-
|
|
751
|
-
it('renders image when provided', () => {
|
|
752
|
-
render(
|
|
753
|
-
<ExpandableImage
|
|
754
|
-
previewSrc="data:image/png;base64,test"
|
|
755
|
-
expandLabel="Expand"
|
|
756
|
-
/>
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
const img = document.querySelector('img')
|
|
760
|
-
expect(img).toHaveAttribute('src', 'data:image/png;base64,test')
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
it('shows expand button on hover', async () => {
|
|
764
|
-
render(
|
|
765
|
-
<ExpandableImage
|
|
766
|
-
previewSrc="data:image/png;base64,test"
|
|
767
|
-
expandLabel="Expand"
|
|
768
|
-
/>
|
|
769
|
-
)
|
|
770
|
-
|
|
771
|
-
const expandButton = screen.getByText('Expand')
|
|
772
|
-
expect(expandButton).toBeInTheDocument()
|
|
773
|
-
})
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
describe('AttachmentImagePreview', () => {
|
|
777
|
-
it('renders header and image', () => {
|
|
778
|
-
render(
|
|
779
|
-
<AttachmentImagePreview
|
|
780
|
-
src="test.jpg"
|
|
781
|
-
header="Test Image"
|
|
782
|
-
/>
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
expect(screen.getByText('Test Image')).toBeInTheDocument()
|
|
786
|
-
expect(document.querySelector('img')).toHaveAttribute('src', 'test.jpg')
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
it('renders action buttons', () => {
|
|
790
|
-
render(
|
|
791
|
-
<AttachmentImagePreview
|
|
792
|
-
src="test.jpg"
|
|
793
|
-
header="Image"
|
|
794
|
-
>
|
|
795
|
-
<button>Save</button>
|
|
796
|
-
<button>Cancel</button>
|
|
797
|
-
</AttachmentImagePreview>
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
expect(screen.getByText('Save')).toBeInTheDocument()
|
|
801
|
-
expect(screen.getByText('Cancel')).toBeInTheDocument()
|
|
802
|
-
})
|
|
803
|
-
})
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
## Accessibility
|
|
807
|
-
|
|
808
|
-
- **Keyboard Navigation**: Full keyboard support for file input
|
|
809
|
-
- **Screen Readers**: Labels announced correctly
|
|
810
|
-
- **ARIA Attributes**: Proper button roles and labels
|
|
811
|
-
- **Focus Management**: Focus trapped in modal when expanded
|
|
812
|
-
- **File Input**: Hidden but accessible to assistive tech
|
|
813
|
-
|
|
814
|
-
### Accessibility Best Practices
|
|
815
|
-
|
|
816
|
-
```typescript
|
|
817
|
-
// Provide clear labels
|
|
818
|
-
<ImageAttachment
|
|
819
|
-
mainLabel="Upload Profile Picture"
|
|
820
|
-
secondaryLabel="JPEG or PNG, max 5MB"
|
|
821
|
-
expandLabel="View full size"
|
|
822
|
-
aria-label="Upload profile picture"
|
|
823
|
-
/>
|
|
824
|
-
|
|
825
|
-
// Add file type context
|
|
826
|
-
<ImageAttachment
|
|
827
|
-
mainLabel="Upload Document"
|
|
828
|
-
secondaryLabel="Accepted: PDF, DOC, DOCX (max 10MB)"
|
|
829
|
-
expandLabel="Preview"
|
|
830
|
-
accept=".pdf,.doc,.docx"
|
|
831
|
-
/>
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
## Performance
|
|
835
|
-
|
|
836
|
-
| Metric | Value |
|
|
837
|
-
|--------|-------|
|
|
838
|
-
| Bundle size (minified) | ~8kb |
|
|
839
|
-
| Bundle size (gzipped) | ~3kb |
|
|
840
|
-
| Dependencies | AlertDialog, Button |
|
|
841
|
-
| Image processing | Client-side (FileReader) |
|
|
842
|
-
| Tree-shakeable | ✅ |
|
|
843
|
-
|
|
844
|
-
### Performance Tips
|
|
845
|
-
|
|
846
|
-
1. **Optimize image size**: Compress before upload
|
|
847
|
-
2. **Lazy load previews**: Only generate when needed
|
|
848
|
-
3. **Limit simultaneous uploads**: Queue multiple files
|
|
849
|
-
4. **Use object URLs**: More efficient than data URLs for large files
|
|
850
|
-
```typescript
|
|
851
|
-
const objectUrl = URL.createObjectURL(file)
|
|
852
|
-
// Remember to revoke: URL.revokeObjectURL(objectUrl)
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
## Styling
|
|
856
|
-
|
|
857
|
-
### Custom Styles
|
|
858
|
-
|
|
859
|
-
```typescript
|
|
860
|
-
// Custom drop zone
|
|
861
|
-
<ImageAttachment
|
|
862
|
-
className="bg-blue-50 border-blue-300"
|
|
863
|
-
mainLabel="Drop here"
|
|
864
|
-
secondaryLabel="Custom styled"
|
|
865
|
-
expandLabel="View"
|
|
866
|
-
/>
|
|
867
|
-
|
|
868
|
-
// Custom preview size
|
|
869
|
-
<ExpandableImage
|
|
870
|
-
className="w-32 h-32"
|
|
871
|
-
previewSrc={preview}
|
|
872
|
-
expandLabel="Expand"
|
|
873
|
-
/>
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
### Default Styling
|
|
877
|
-
|
|
878
|
-
Matches the *Attachment-Field-1.0 / Pic-Container-1.0* design:
|
|
879
|
-
|
|
880
|
-
- **Field box** (`ImageAttachment`): a `#f9f9f9` (`background-presentation-form-field-primary`)
|
|
881
|
-
rounded container holding an optional preview and the drop zone, `gap-[10px]`.
|
|
882
|
-
- **Drop zone**: fills the box (`flex-1`), 65px tall, transparent with a dashed
|
|
883
|
-
`border-presentation-action-borderstyle` border; drag-active tints it gray.
|
|
884
|
-
- **Preview thumbnail** (`ExpandableImage`): a fixed **65×65** square, `object-cover`, rounded.
|
|
885
|
-
- **Placeholder** (no image): ocean fill (`badge-ocean-subtle`) + dashed `blue-purple-300`
|
|
886
|
-
border + paperclip icon; on hover it goes gray (`badge-gray-subtle` / `badge-gray`).
|
|
887
|
-
- **Hover (has image)**: a `black/50` overlay with a fullscreen icon + the `expandLabel`.
|
|
888
|
-
|
|
889
|
-
## Best Practices
|
|
890
|
-
|
|
891
|
-
1. **Validate files**: Check type and size before processing
|
|
892
|
-
2. **Provide feedback**: Show upload progress and errors
|
|
893
|
-
3. **Clear labels**: Describe accepted formats and limits
|
|
894
|
-
4. **Handle errors**: Gracefully handle file read failures
|
|
895
|
-
5. **Optimize images**: Compress or resize on client before upload
|
|
896
|
-
6. **Cleanup**: Revoke object URLs when done
|
|
897
|
-
7. **Accessibility**: Ensure keyboard and screen reader support
|
|
898
|
-
8. **Multiple files**: Consider UX for bulk uploads
|
|
899
|
-
|
|
900
|
-
## Related Components
|
|
901
|
-
|
|
902
|
-
- [Button](./button.md) - Action buttons
|
|
903
|
-
- [AlertDialog](./alert-dialog.md) - Modal container for preview
|
|
904
|
-
- [Avatar](./avatar.md) - Profile picture display
|
|
905
|
-
- [FieldHint](./field-hint.md) - Validation messages
|
|
906
|
-
- [Toast](./toast.md) - Upload notifications
|