@flikk/ui 1.0.0-beta.25 → 1.0.0-beta.27
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/dist/components/ai/CodeBlock/CodeBlock.theme.js +2 -2
- package/dist/components/ai/PromptInput/PromptInput.js +4 -0
- package/dist/components/ai/PromptInput/VoiceRecorder.js +4 -0
- package/dist/components/ai/StreamingResponse/MarkdownRenderer.js +1 -1
- package/dist/components/ai/TokenCounter/TokenCounter.theme.js +1 -1
- package/dist/components/charts/BarChart/BarChart.js +2 -2
- package/dist/components/charts/StackedBarChart/StackedBarChart.js +1 -1
- package/dist/components/charts/shared/BarRenderer/BarRenderer.js +2 -2
- package/dist/components/core/Breadcrumbs/Breadcrumbs.theme.js +1 -1
- package/dist/components/core/Button/Button.js +1 -1
- package/dist/components/core/Button/Button.theme.js +5 -5
- package/dist/components/core/Calendar/Calendar.theme.js +1 -1
- package/dist/components/core/Calendar/CalendarMini/CalendarMini.theme.js +1 -1
- package/dist/components/core/CommandPalette/CommandItem.js +4 -5
- package/dist/components/core/CommandPalette/CommandPalette.js +1 -1
- package/dist/components/core/CommandPalette/CommandPalette.theme.js +11 -13
- package/dist/components/core/CommandPalette/CommandPalette.types.d.ts +0 -2
- package/dist/components/core/ContextMenu/ContextMenu.theme.js +13 -14
- package/dist/components/core/DotSeparator/DotSeparator.d.ts +3 -0
- package/dist/components/core/DotSeparator/DotSeparator.js +19 -0
- package/dist/components/core/DotSeparator/DotSeparator.theme.d.ts +2 -0
- package/dist/components/core/DotSeparator/DotSeparator.theme.js +10 -0
- package/dist/components/core/DotSeparator/DotSeparator.types.d.ts +25 -0
- package/dist/components/core/DotSeparator/index.d.ts +3 -0
- package/dist/components/core/Drawer/Drawer.theme.js +2 -2
- package/dist/components/core/Dropdown/Dropdown.theme.js +4 -5
- package/dist/components/core/Dropdown/DropdownItem.js +1 -1
- package/dist/components/core/Dropdown/DropdownMenu.js +1 -1
- package/dist/components/core/Empty/Empty.js +1 -1
- package/dist/components/core/ImagePlaceholder/ImagePlaceholder.d.ts +14 -0
- package/dist/components/core/ImagePlaceholder/ImagePlaceholder.js +28 -0
- package/dist/components/core/ImagePlaceholder/ImagePlaceholder.theme.d.ts +6 -0
- package/dist/components/core/ImagePlaceholder/ImagePlaceholder.theme.js +8 -0
- package/dist/components/core/ImagePlaceholder/ImagePlaceholder.types.d.ts +32 -0
- package/dist/components/core/ImagePlaceholder/index.d.ts +2 -0
- package/dist/components/core/MenuItem/MenuItem.js +5 -2
- package/dist/components/core/MenuItem/MenuItem.theme.d.ts +6 -0
- package/dist/components/core/MenuItem/MenuItem.theme.js +19 -16
- package/dist/components/core/MenuItem/MenuItem.types.d.ts +2 -0
- package/dist/components/core/Message/Message.theme.js +1 -1
- package/dist/components/core/Modal/Modal.theme.js +1 -1
- package/dist/components/core/NavItem/NavItem.d.ts +1 -1
- package/dist/components/core/NavItem/NavItem.js +187 -68
- package/dist/components/core/NavItem/NavItem.theme.js +31 -12
- package/dist/components/core/NavItem/NavItem.types.d.ts +17 -39
- package/dist/components/core/Pagination/Pagination.theme.js +3 -3
- package/dist/components/core/Popover/Popover.theme.js +1 -1
- package/dist/components/core/Popover/Popover.types.d.ts +6 -2
- package/dist/components/core/Popover/PopoverBody.js +10 -3
- package/dist/components/core/Progress/Progress.theme.js +1 -1
- package/dist/components/core/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/core/ScrollArea/ScrollArea.theme.js +1 -1
- package/dist/components/core/Segmented/Segmented.js +8 -6
- package/dist/components/core/Segmented/Segmented.theme.js +3 -3
- package/dist/components/core/Sidebar/Sidebar.d.ts +3 -1
- package/dist/components/core/Sidebar/Sidebar.js +53 -16
- package/dist/components/core/Sidebar/Sidebar.theme.js +5 -7
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +125 -8
- package/dist/components/core/Sidebar/SidebarCard.d.ts +25 -0
- package/dist/components/core/Sidebar/SidebarCard.js +68 -0
- package/dist/components/core/Sidebar/SidebarCard.theme.d.ts +8 -0
- package/dist/components/core/Sidebar/SidebarCard.theme.js +18 -0
- package/dist/components/core/Sidebar/SidebarContent.js +10 -6
- package/dist/components/core/Sidebar/SidebarContext.d.ts +17 -16
- package/dist/components/core/Sidebar/SidebarContext.js +43 -20
- package/dist/components/core/Sidebar/SidebarFooter.js +4 -6
- package/dist/components/core/Sidebar/SidebarHeader.js +12 -9
- package/dist/components/core/Sidebar/SidebarLogo.js +39 -8
- package/dist/components/core/Sidebar/SidebarNav.js +4 -3
- package/dist/components/core/Sidebar/SidebarNavGroup.js +4 -7
- package/dist/components/core/Sidebar/SidebarSearch.d.ts +22 -0
- package/dist/components/core/Sidebar/SidebarSearch.js +135 -0
- package/dist/components/core/Sidebar/SidebarToggle.js +6 -1
- package/dist/components/core/Sidebar/SidebarUserProfile.js +33 -17
- package/dist/components/core/Sidebar/index.d.ts +4 -3
- package/dist/components/core/Skeleton/Skeleton.theme.js +1 -1
- package/dist/components/core/Spinner/Spinner.theme.js +1 -1
- package/dist/components/core/Tabs/Tabs.js +6 -1
- package/dist/components/core/Tabs/Tabs.types.d.ts +2 -0
- package/dist/components/core/Tabs/TabsBody.js +5 -3
- package/dist/components/core/Tag/Tag.js +29 -5
- package/dist/components/core/Tag/Tag.theme.js +14 -9
- package/dist/components/core/Tag/Tag.types.d.ts +7 -0
- package/dist/components/core/Tooltip/Tooltip.js +1 -1
- package/dist/components/core/Tooltip/Tooltip.theme.js +1 -2
- package/dist/components/core/index.d.ts +2 -0
- package/dist/components/core/index.js +6 -2
- package/dist/components/data-display/GanttChart/GanttChart.d.ts +1 -1
- package/dist/components/data-display/GanttChart/GanttChart.js +31 -4
- package/dist/components/data-display/GanttChart/GanttChart.theme.js +19 -17
- package/dist/components/data-display/GanttChart/GanttChart.types.d.ts +19 -0
- package/dist/components/data-display/GanttChart/GanttChart.utils.js +1 -1
- package/dist/components/data-display/GanttChart/GanttChartPanel.d.ts +1 -1
- package/dist/components/data-display/GanttChart/GanttChartPanel.js +3 -3
- package/dist/components/data-display/GanttChart/GanttDependencyArrows.js +7 -4
- package/dist/components/data-display/GanttChart/GanttMilestone.js +5 -5
- package/dist/components/data-display/GanttChart/GanttTableGroupRow.js +1 -1
- package/dist/components/data-display/GanttChart/GanttTablePanel.js +1 -1
- package/dist/components/data-display/GanttChart/GanttTableTaskRow.js +2 -2
- package/dist/components/data-display/GanttChart/GanttTaskBar.js +37 -16
- package/dist/components/data-display/GanttChart/GanttTimelineGrid.js +2 -1
- package/dist/components/data-display/GanttChart/GanttTimelineHeader.js +35 -11
- package/dist/components/data-display/GanttChart/GanttToolbar.js +2 -2
- package/dist/components/data-display/GanttChart/hooks/useGanttDrag.js +3 -2
- package/dist/components/data-display/GanttChart/index.d.ts +1 -1
- package/dist/components/data-display/KPI/KPI.js +13 -3
- package/dist/components/data-display/KPI/KPI.theme.js +3 -3
- package/dist/components/data-display/Metric/Metric.theme.js +3 -3
- package/dist/components/data-display/Table/Table.js +4 -3
- package/dist/components/data-display/Table/Table.theme.js +14 -6
- package/dist/components/data-display/Table/Table.types.d.ts +15 -0
- package/dist/components/data-display/Table/TableColumnManager.js +1 -1
- package/dist/components/data-display/Table/index.d.ts +2 -2
- package/dist/components/effects/Aurora/Aurora.d.ts +6 -17
- package/dist/components/effects/Aurora/Aurora.js +310 -105
- package/dist/components/effects/Aurora/Aurora.types.d.ts +6 -0
- package/dist/components/effects/InteractiveCharacters/InteractiveCharacters.js +3 -3
- package/dist/components/effects/InteractiveCharacters/characters/BlobCharacter.js +13 -4
- package/dist/components/effects/InteractiveCharacters/characters/SmallCharacter.js +13 -4
- package/dist/components/effects/InteractiveCharacters/characters/TallCharacter.js +14 -4
- package/dist/components/effects/MorphingText/MorphingText.js +40 -7
- package/dist/components/effects/StripePattern/StripePattern.d.ts +18 -0
- package/dist/components/effects/StripePattern/StripePattern.js +54 -0
- package/dist/components/effects/StripePattern/StripePattern.types.d.ts +53 -0
- package/dist/components/effects/StripePattern/index.d.ts +2 -0
- package/dist/components/effects/index.d.ts +2 -0
- package/dist/components/effects/index.js +1 -0
- package/dist/components/forms/Checkbox/Checkbox.theme.js +1 -1
- package/dist/components/forms/Combobox/Combobox.js +1 -1
- package/dist/components/forms/Combobox/Combobox.theme.js +2 -4
- package/dist/components/forms/CronInput/CronInput.js +1 -1
- package/dist/components/forms/CronInput/CronInput.theme.js +22 -31
- package/dist/components/forms/DatePicker/DatePicker.theme.js +11 -18
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +1 -1
- package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +7 -14
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +1 -1
- package/dist/components/forms/FileUpload/FileUpload.animations.d.ts +29 -0
- package/dist/components/forms/FileUpload/FileUpload.animations.js +19 -0
- package/dist/components/forms/FileUpload/FileUpload.js +88 -49
- package/dist/components/forms/FileUpload/FileUpload.theme.d.ts +2 -10
- package/dist/components/forms/FileUpload/FileUpload.theme.js +16 -30
- package/dist/components/forms/FileUpload/FileUpload.types.d.ts +26 -10
- package/dist/components/forms/FileUpload/FileUploadProgress.js +27 -20
- package/dist/components/forms/FileUpload/FileUploadProgress.theme.js +2 -2
- package/dist/components/forms/Input/Input.theme.js +16 -15
- package/dist/components/forms/InputCounter/InputCounter.theme.js +1 -1
- package/dist/components/forms/Mention/Mention.js +152 -57
- package/dist/components/forms/Mention/Mention.theme.js +16 -23
- package/dist/components/forms/Mention/Mention.types.d.ts +12 -12
- package/dist/components/forms/Mention/Mention.utils.js +32 -4
- package/dist/components/forms/Radio/Radio.theme.js +1 -1
- package/dist/components/forms/RichTextEditor/RichTextEditor.d.ts +18 -0
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +1231 -236
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.d.ts +53 -1
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +99 -25
- package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +42 -3
- package/dist/components/forms/RichTextEditor/index.d.ts +2 -2
- package/dist/components/forms/Select/Select.js +2 -2
- package/dist/components/forms/Select/Select.theme.js +3 -3
- package/dist/components/forms/Slider/Slider.theme.js +1 -1
- package/dist/components/forms/Switch/Switch.theme.js +1 -1
- package/dist/components/forms/TimePicker/TimePicker.theme.js +2 -2
- package/dist/components/forms/TimePicker/TimePickerContent.js +4 -0
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +1 -1
- package/dist/components/forms/forms.theme.js +24 -19
- package/dist/components/forms/index.js +4 -0
- package/dist/components/layout/AppShell/AppShell.d.ts +10 -0
- package/dist/components/layout/AppShell/AppShell.js +84 -0
- package/dist/components/layout/AppShell/AppShell.theme.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShell.theme.js +42 -0
- package/dist/components/layout/AppShell/AppShell.types.d.ts +127 -0
- package/dist/components/layout/AppShell/AppShellContext.d.ts +14 -0
- package/dist/components/layout/AppShell/AppShellContext.js +19 -0
- package/dist/components/layout/AppShell/AppShellFooter.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellFooter.js +12 -0
- package/dist/components/layout/AppShell/AppShellHeader.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellHeader.js +12 -0
- package/dist/components/layout/AppShell/AppShellMain.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellMain.js +19 -0
- package/dist/components/layout/AppShell/AppShellRightPanel.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellRightPanel.js +16 -0
- package/dist/components/layout/AppShell/AppShellSidebar.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellSidebar.js +16 -0
- package/dist/components/layout/AppShell/AppShellTopbar.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellTopbar.js +12 -0
- package/dist/components/layout/AppShell/index.d.ts +3 -0
- package/dist/components/layout/FormLayout/FormLayout.js +9 -4
- package/dist/components/layout/FormLayout/FormLayout.types.d.ts +3 -3
- package/dist/components/layout/FormLayout/FormLayoutSection.js +6 -1
- package/dist/components/layout/PageLayout/PageLayout.d.ts +2 -2
- package/dist/components/layout/PageLayout/PageLayout.js +23 -5
- package/dist/components/layout/PageLayout/PageLayout.theme.d.ts +2 -1
- package/dist/components/layout/PageLayout/PageLayout.theme.js +35 -6
- package/dist/components/layout/PageLayout/PageLayout.types.d.ts +45 -0
- package/dist/components/layout/PageLayout/PageLayoutContent.js +6 -4
- package/dist/components/layout/PageLayout/PageLayoutHeader.js +3 -3
- package/dist/components/layout/PageLayout/PageLayoutSidebar.js +5 -3
- package/dist/components/layout/PageLayout/index.d.ts +1 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/hooks/useSelectPortal.d.ts +10 -2
- package/dist/hooks/useSelectPortal.js +45 -24
- package/dist/index.js +8 -2
- package/dist/registry.json +10021 -0
- package/dist/styles.css +1 -1
- package/dist/utils/dateUtils.js +11 -1
- package/package.json +4 -2
- package/src/global.scss +29 -8
- package/src/styles/theme.css +174 -100
- package/dist/components/core/Sidebar/SidebarSubmenu.d.ts +0 -7
- package/dist/components/core/Sidebar/SidebarSubmenu.js +0 -12
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useMemo, useId, useRef, useState,
|
|
2
|
+
import React__default, { useMemo, useId, useRef, useCallback, useState, useEffect } from 'react';
|
|
3
|
+
import { useReducedMotion, AnimatePresence, motion } from 'motion/react';
|
|
3
4
|
import { fileUploadTheme } from './FileUpload.theme.js';
|
|
5
|
+
import { PREVIEW_LAYOUT_SPRING, PREVIEW_ITEM } from './FileUpload.animations.js';
|
|
4
6
|
import { cn } from '../../../utils/cn.js';
|
|
5
7
|
import { CloudArrowUpIcon, DocumentIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
6
8
|
import '../../core/Accordion/Accordion.js';
|
|
@@ -32,6 +34,7 @@ import '../../core/Dropdown/DropdownItem.js';
|
|
|
32
34
|
import '../../core/Dropdown/DropdownSection.js';
|
|
33
35
|
import '../../core/Dropdown/DropdownSeparator.js';
|
|
34
36
|
import '../../core/Dropdown/Dropdown.theme.js';
|
|
37
|
+
import '../../core/DotSeparator/DotSeparator.js';
|
|
35
38
|
import '../../core/Kbd/Kbd.js';
|
|
36
39
|
import '../../core/MenuItem/MenuItem.js';
|
|
37
40
|
import '../../core/Link/Link.js';
|
|
@@ -78,17 +81,19 @@ import '../../core/Sidebar/SidebarContent.js';
|
|
|
78
81
|
import '../../core/Sidebar/SidebarFooter.js';
|
|
79
82
|
import '../../core/Sidebar/SidebarNav.js';
|
|
80
83
|
import '../../core/Sidebar/SidebarNavGroup.js';
|
|
81
|
-
import 'motion/react';
|
|
82
84
|
import '../../core/Sidebar/SidebarToggle.js';
|
|
83
85
|
import '../../core/Sidebar/SidebarMobileTrigger.js';
|
|
84
86
|
import '../../core/Sidebar/SidebarUserProfile.js';
|
|
85
87
|
import '../../core/Sidebar/SidebarLogo.js';
|
|
88
|
+
import '../../core/Sidebar/SidebarSearch.js';
|
|
89
|
+
import '../../core/Sidebar/SidebarCard.js';
|
|
86
90
|
import '../../core/Sidebar/SidebarContext.js';
|
|
87
91
|
import '../../core/OfflineIndicator/OfflineIndicator.js';
|
|
88
92
|
import '../../core/ContextMenu/ContextMenu.js';
|
|
89
93
|
import '../../core/ContextMenu/ContextMenu.theme.js';
|
|
90
94
|
import '../../core/Pill/Pill.js';
|
|
91
95
|
import '../../core/Icon/Icon.js';
|
|
96
|
+
import '../../core/ImagePlaceholder/ImagePlaceholder.js';
|
|
92
97
|
import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
93
98
|
|
|
94
99
|
/**
|
|
@@ -111,7 +116,7 @@ import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
|
111
116
|
* onUpload={(files) => console.log(files)}
|
|
112
117
|
* />
|
|
113
118
|
*/
|
|
114
|
-
const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSize, maxFiles, state = "default", disabled = false, label, errorMessage, onUpload, onRemove, onError, showPreview = true, buttonText = "Choose files", helperText = "or drag and drop", description, className, theme: customTheme, ...props }, ref) => {
|
|
119
|
+
const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSize, maxFiles, state = "default", disabled = false, label, errorMessage, value, defaultFiles, onChange, onUpload, onRemove, onError, showPreview = true, showIcon = true, buttonText = "Choose files", helperText = "or drag and drop", description, className, theme: customTheme, ...props }, ref) => {
|
|
115
120
|
// Merge custom theme with default theme
|
|
116
121
|
const theme = useMemo(() => customTheme
|
|
117
122
|
? { ...fileUploadTheme, ...customTheme }
|
|
@@ -119,10 +124,39 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
|
|
|
119
124
|
const generatedId = useId();
|
|
120
125
|
const isDisabled = state === "disabled" || disabled;
|
|
121
126
|
const isInvalid = state === "invalid";
|
|
127
|
+
// Wire helper/error text to the control for screen readers.
|
|
128
|
+
const describedBy = [
|
|
129
|
+
description ? `${generatedId}-description` : null,
|
|
130
|
+
errorMessage ? `${generatedId}-error` : null,
|
|
131
|
+
]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(" ") || undefined;
|
|
134
|
+
const shouldReduceMotion = useReducedMotion();
|
|
135
|
+
// Stable key per File reference — index keys would break exit/reflow
|
|
136
|
+
// animations as the list reorders. WeakMap so removed files are GC'd.
|
|
137
|
+
const fileKeysRef = useRef(new WeakMap());
|
|
138
|
+
const keyCounterRef = useRef(0);
|
|
139
|
+
const getFileKey = useCallback((file) => {
|
|
140
|
+
let key = fileKeysRef.current.get(file);
|
|
141
|
+
if (!key) {
|
|
142
|
+
key = `file-${keyCounterRef.current++}`;
|
|
143
|
+
fileKeysRef.current.set(file, key);
|
|
144
|
+
}
|
|
145
|
+
return key;
|
|
146
|
+
}, []);
|
|
122
147
|
const inputRef = useRef(null);
|
|
123
148
|
const [isDragActive, setIsDragActive] = useState(false);
|
|
124
|
-
|
|
149
|
+
// Controlled when `value` is provided; otherwise track the list internally.
|
|
150
|
+
const isControlled = value !== undefined;
|
|
151
|
+
const [internalFiles, setInternalFiles] = useState(defaultFiles !== null && defaultFiles !== void 0 ? defaultFiles : []);
|
|
152
|
+
const selectedFiles = isControlled ? value : internalFiles;
|
|
125
153
|
const [previewUrls, setPreviewUrls] = useState(new Map());
|
|
154
|
+
// Commit a new list: update internal state when uncontrolled, always notify.
|
|
155
|
+
const commitFiles = useCallback((next) => {
|
|
156
|
+
if (!isControlled)
|
|
157
|
+
setInternalFiles(next);
|
|
158
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(next);
|
|
159
|
+
}, [isControlled, onChange]);
|
|
126
160
|
// Convert accept prop to array
|
|
127
161
|
const acceptTypes = Array.isArray(accept) ? accept : accept ? [accept] : [];
|
|
128
162
|
const acceptString = acceptTypes.join(",");
|
|
@@ -150,25 +184,40 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
|
|
|
150
184
|
return true;
|
|
151
185
|
return file.size <= maxSize;
|
|
152
186
|
}, [maxSize]);
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
187
|
+
// Keep image preview URLs in sync with the current list. Runs for files added
|
|
188
|
+
// via the dropzone as well as files injected through `value`/`defaultFiles`.
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
setPreviewUrls((prev) => {
|
|
191
|
+
const next = new Map();
|
|
192
|
+
let changed = false;
|
|
193
|
+
for (const file of selectedFiles) {
|
|
194
|
+
if (!file.type.startsWith("image/"))
|
|
195
|
+
continue;
|
|
196
|
+
const existing = prev.get(file);
|
|
197
|
+
if (existing) {
|
|
198
|
+
next.set(file, existing);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
next.set(file, URL.createObjectURL(file));
|
|
202
|
+
changed = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Revoke URLs for files no longer present.
|
|
206
|
+
for (const [file, url] of prev) {
|
|
207
|
+
if (!next.has(file)) {
|
|
208
|
+
URL.revokeObjectURL(url);
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return changed ? next : prev;
|
|
213
|
+
});
|
|
214
|
+
}, [selectedFiles]);
|
|
215
|
+
// Revoke any outstanding object URLs on unmount.
|
|
216
|
+
const previewUrlsRef = useRef(previewUrls);
|
|
217
|
+
previewUrlsRef.current = previewUrls;
|
|
218
|
+
useEffect(() => () => {
|
|
219
|
+
previewUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
|
159
220
|
}, []);
|
|
160
|
-
// Clean up preview URLs
|
|
161
|
-
const revokePreview = useCallback((file) => {
|
|
162
|
-
const url = previewUrls.get(file);
|
|
163
|
-
if (url) {
|
|
164
|
-
URL.revokeObjectURL(url);
|
|
165
|
-
setPreviewUrls((prev) => {
|
|
166
|
-
const newMap = new Map(prev);
|
|
167
|
-
newMap.delete(file);
|
|
168
|
-
return newMap;
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}, [previewUrls]);
|
|
172
221
|
// Handle file selection/drop
|
|
173
222
|
const handleFiles = useCallback((files) => {
|
|
174
223
|
if (!files || files.length === 0)
|
|
@@ -209,14 +258,13 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
|
|
|
209
258
|
onError === null || onError === void 0 ? void 0 : onError(error);
|
|
210
259
|
return;
|
|
211
260
|
}
|
|
212
|
-
//
|
|
261
|
+
// Append when multiple, otherwise replace. Previews are handled by the
|
|
262
|
+
// sync effect above.
|
|
213
263
|
const newFiles = multiple
|
|
214
264
|
? [...selectedFiles, ...validFiles]
|
|
215
265
|
: validFiles;
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
validFiles.forEach(createPreview);
|
|
219
|
-
// Call onUpload callback
|
|
266
|
+
commitFiles(newFiles);
|
|
267
|
+
// Call onUpload callback with only the newly added files
|
|
220
268
|
onUpload === null || onUpload === void 0 ? void 0 : onUpload(validFiles);
|
|
221
269
|
}, [
|
|
222
270
|
isValidFileType,
|
|
@@ -227,7 +275,7 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
|
|
|
227
275
|
multiple,
|
|
228
276
|
onUpload,
|
|
229
277
|
onError,
|
|
230
|
-
|
|
278
|
+
commitFiles,
|
|
231
279
|
]);
|
|
232
280
|
// Handle drag events
|
|
233
281
|
const handleDragEnter = useCallback((e) => {
|
|
@@ -269,32 +317,23 @@ const FileUpload = React__default.forwardRef(({ accept, multiple = false, maxSiz
|
|
|
269
317
|
}, [handleFiles]);
|
|
270
318
|
// Handle file removal
|
|
271
319
|
const handleRemoveFile = useCallback((file) => {
|
|
272
|
-
|
|
273
|
-
|
|
320
|
+
// Preview cleanup is handled by the sync effect.
|
|
321
|
+
commitFiles(selectedFiles.filter((f) => f !== file));
|
|
274
322
|
onRemove === null || onRemove === void 0 ? void 0 : onRemove(file);
|
|
275
|
-
}, [
|
|
276
|
-
|
|
277
|
-
const formatFileSize = (bytes) => {
|
|
278
|
-
if (bytes === 0)
|
|
279
|
-
return "0 Bytes";
|
|
280
|
-
const k = 1024;
|
|
281
|
-
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
282
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
283
|
-
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
284
|
-
};
|
|
285
|
-
return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, className), ...props, children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-input`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", children: label })) : (label)), jsx("input", { ref: inputRef, id: `${generatedId}-input`, type: "file", accept: acceptString, multiple: multiple, onChange: handleInputChange, disabled: isDisabled, className: "sr-only", "aria-label": "File upload" }), jsx("div", { className: cn(theme.dropzoneStyle, isDragActive && theme.dropzoneActiveStyle, isDisabled && theme.dropzoneDisabledStyle, isInvalid && "border-[var(--color-danger)]"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleClick, role: "button", tabIndex: isDisabled ? -1 : 0, "aria-disabled": isDisabled, "aria-invalid": isInvalid || undefined, onKeyDown: (e) => {
|
|
323
|
+
}, [commitFiles, selectedFiles, onRemove]);
|
|
324
|
+
return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, className), ...props, children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-input`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", children: label })) : (label)), jsx("input", { ref: inputRef, id: `${generatedId}-input`, type: "file", accept: acceptString, multiple: multiple, onChange: handleInputChange, disabled: isDisabled, className: "sr-only", "aria-label": "File upload", "aria-invalid": isInvalid || undefined, "aria-describedby": describedBy }), jsx("div", { className: cn(theme.dropzoneStyle, isDragActive && theme.dropzoneActiveStyle, isDisabled && theme.dropzoneDisabledStyle, isInvalid && "border-[var(--color-danger)]"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleClick, role: "button", tabIndex: isDisabled ? -1 : 0, "aria-disabled": isDisabled, "aria-invalid": isInvalid || undefined, "aria-describedby": describedBy, onKeyDown: (e) => {
|
|
286
325
|
if (e.key === "Enter" || e.key === " ") {
|
|
287
326
|
e.preventDefault();
|
|
288
327
|
handleClick();
|
|
289
328
|
}
|
|
290
|
-
}, children: jsxs("div", { className:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
329
|
+
}, children: jsxs("div", { className: "flex flex-col items-center text-center pointer-events-none", children: [showIcon && (jsx("div", { className: "size-10 border border-[var(--color-border)] bg-[var(--color-surface-raised)] rounded-full flex items-center justify-center mb-3", children: jsx(CloudArrowUpIcon, { className: "size-6 text-[var(--color-text-secondary)]" }) })), jsxs("p", { className: "text-sm font-medium text-[var(--color-text-primary)]/80", children: [jsx("span", { className: "text-[var(--color-primary)]", children: buttonText }), " ", helperText] }), description && (jsx("p", { id: `${generatedId}-description`, className: "text-xs text-[var(--color-text-muted)] mt-0.5", children: description }))] }) }), errorMessage && (jsx("div", { id: `${generatedId}-error`, className: "text-sm mt-1 text-[var(--color-danger)]", children: errorMessage })), showPreview && selectedFiles.length > 0 && (jsx("div", { className: theme.previewContainerStyle, children: jsx(AnimatePresence, { initial: false, children: selectedFiles.map((file) => {
|
|
330
|
+
const previewUrl = previewUrls.get(file);
|
|
331
|
+
const isImage = file.type.startsWith("image/");
|
|
332
|
+
return (jsxs(motion.div, { layout: !shouldReduceMotion, initial: shouldReduceMotion ? false : PREVIEW_ITEM.initial, animate: PREVIEW_ITEM.animate, exit: shouldReduceMotion ? { opacity: 0 } : PREVIEW_ITEM.exit, transition: PREVIEW_LAYOUT_SPRING, className: theme.previewItemStyle, children: [isImage && previewUrl ? (jsx("img", { src: previewUrl, alt: file.name, className: theme.previewImageStyle })) : (jsx("div", { className: theme.previewFileStyle, title: file.name, children: jsx(DocumentIcon, { className: "size-6" }) })), jsx(Button, { variant: "soft", color: "danger", size: "sm", iconOnly: true, onClick: (e) => {
|
|
333
|
+
e.stopPropagation();
|
|
334
|
+
handleRemoveFile(file);
|
|
335
|
+
}, "aria-label": `Remove ${file.name}`, className: cn(theme.removeButtonStyle, "rounded-full !p-0 size-6"), children: jsx(XMarkIcon, { className: "size-4", strokeWidth: 2 }) })] }, getFileKey(file)));
|
|
336
|
+
}) }) }))] }));
|
|
298
337
|
});
|
|
299
338
|
FileUpload.displayName = "FileUpload";
|
|
300
339
|
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Theme for the FileUpload component.
|
|
3
|
+
* Uses production-ready CSS variables from theme.css.
|
|
3
4
|
*/
|
|
4
5
|
interface FileUploadTheme {
|
|
5
6
|
containerStyle: string;
|
|
6
7
|
dropzoneStyle: string;
|
|
7
8
|
dropzoneActiveStyle: string;
|
|
8
9
|
dropzoneDisabledStyle: string;
|
|
9
|
-
contentStyle: string;
|
|
10
|
-
iconStyle: string;
|
|
11
|
-
textStyle: string;
|
|
12
|
-
helperTextStyle: string;
|
|
13
|
-
descriptionStyle: string;
|
|
14
10
|
previewContainerStyle: string;
|
|
15
11
|
previewItemStyle: string;
|
|
16
12
|
previewImageStyle: string;
|
|
17
13
|
previewFileStyle: string;
|
|
18
14
|
removeButtonStyle: string;
|
|
19
15
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Default theme for the FileUpload component
|
|
22
|
-
* Uses production-ready CSS variables from global.scss
|
|
23
|
-
*/
|
|
24
16
|
export declare const fileUploadTheme: FileUploadTheme;
|
|
25
17
|
export {};
|
|
@@ -1,36 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default theme for the FileUpload component
|
|
3
|
-
* Uses production-ready CSS variables from global.scss
|
|
4
|
-
*/
|
|
5
1
|
const fileUploadTheme = {
|
|
6
|
-
//
|
|
2
|
+
// Root container
|
|
7
3
|
containerStyle: "w-full",
|
|
8
4
|
// Dropzone area (default state)
|
|
9
|
-
dropzoneStyle: "relative flex flex-col items-center justify-center w-full min-h-[160px] p-6 border border-dashed border-[var(--color-border)] rounded-[var(--form-radius)] bg-[var(--color-
|
|
10
|
-
//
|
|
11
|
-
dropzoneActiveStyle: "border-[var(--color-primary)] bg-[var(--color-primary
|
|
12
|
-
//
|
|
13
|
-
dropzoneDisabledStyle: "cursor-not-allowed opacity-60 hover:bg-[var(--color-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// Preview container
|
|
25
|
-
previewContainerStyle: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 mt-4",
|
|
26
|
-
// Individual preview item
|
|
27
|
-
previewItemStyle: "relative group rounded-[var(--radius-base)] border border-[var(--color-border)] overflow-hidden bg-[var(--color-background-secondary)]",
|
|
28
|
-
// Preview image
|
|
29
|
-
previewImageStyle: "w-full h-32 object-cover",
|
|
30
|
-
// Preview file (non-image)
|
|
31
|
-
previewFileStyle: "flex items-center justify-center w-full h-32 p-3 text-sm text-[var(--color-text-secondary)] text-center break-all",
|
|
32
|
-
// Remove button
|
|
33
|
-
removeButtonStyle: "absolute top-1 right-1 -translate-y-1 opacity-0 group-hover:opacity-100 transition-all duration-400 group-hover:-translate-y-0",
|
|
5
|
+
dropzoneStyle: "relative flex flex-col items-center justify-center w-full min-h-[160px] p-6 border border-dashed border-[var(--color-border)] rounded-[var(--form-radius)] bg-[var(--color-surface)] hover:border-[var(--color-border-hover)] transition-colors duration-400 cursor-pointer",
|
|
6
|
+
// Dragging over — translucent primary tint composites over the surface in both modes
|
|
7
|
+
dropzoneActiveStyle: "border-solid border-[var(--color-primary)] bg-[var(--color-primary)]/5",
|
|
8
|
+
// Disabled
|
|
9
|
+
dropzoneDisabledStyle: "cursor-not-allowed opacity-60 hover:bg-[var(--color-surface)] hover:border-[var(--color-border)]",
|
|
10
|
+
// Wrapping row of fixed-width thumbnails (not a stretch grid)
|
|
11
|
+
previewContainerStyle: "flex flex-wrap gap-1 mt-2",
|
|
12
|
+
// Fixed responsive thumbnail — stays small on large containers, wraps to more-per-row
|
|
13
|
+
previewItemStyle: "relative group shrink-0 size-10 sm:size-12 md:size-16 rounded-[var(--radius-base)] border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface)]",
|
|
14
|
+
// Image fills the thumbnail box
|
|
15
|
+
previewImageStyle: "size-full object-cover",
|
|
16
|
+
// Non-image tile — centered document icon (filename surfaced via title tooltip)
|
|
17
|
+
previewFileStyle: "flex items-center justify-center size-full text-[var(--color-text-muted)]",
|
|
18
|
+
// Remove button (revealed on item hover)
|
|
19
|
+
removeButtonStyle: "absolute top-1 right-1 -translate-y-1 opacity-0 group-hover:opacity-100 transition-all duration-400 group-hover:-translate-y-0",
|
|
34
20
|
};
|
|
35
21
|
|
|
36
22
|
export { fileUploadTheme };
|
|
@@ -4,7 +4,7 @@ export type FileUploadState = "default" | "disabled" | "invalid";
|
|
|
4
4
|
* FileUpload component props
|
|
5
5
|
* Supports both click-to-upload and drag-and-drop
|
|
6
6
|
*/
|
|
7
|
-
export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {
|
|
7
|
+
export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError' | 'onChange'> {
|
|
8
8
|
/**
|
|
9
9
|
* Accepted file types (MIME types or file extensions)
|
|
10
10
|
* @example ["image/*", ".pdf", ".doc"]
|
|
@@ -43,7 +43,22 @@ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElemen
|
|
|
43
43
|
*/
|
|
44
44
|
errorMessage?: React.ReactNode;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Controlled file list. When provided, the component is controlled and the
|
|
47
|
+
* parent owns the list — pair with `onChange`. Leave undefined for the
|
|
48
|
+
* uncontrolled behavior (the component tracks its own list internally).
|
|
49
|
+
*/
|
|
50
|
+
value?: File[];
|
|
51
|
+
/**
|
|
52
|
+
* Initial file list for the uncontrolled case. Ignored when `value` is set.
|
|
53
|
+
*/
|
|
54
|
+
defaultFiles?: File[];
|
|
55
|
+
/**
|
|
56
|
+
* Callback fired whenever the full file list changes (add or remove),
|
|
57
|
+
* with the complete next list. Required to read state in controlled mode.
|
|
58
|
+
*/
|
|
59
|
+
onChange?: (files: File[]) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Callback when files are selected/dropped, with only the newly added files.
|
|
47
62
|
*/
|
|
48
63
|
onUpload?: (files: File[]) => void;
|
|
49
64
|
/**
|
|
@@ -59,20 +74,26 @@ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElemen
|
|
|
59
74
|
* @default true
|
|
60
75
|
*/
|
|
61
76
|
showPreview?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Show the upload icon inside the dropzone. Set `false` for slim/compact
|
|
79
|
+
* containers where the icon would crowd the text.
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
showIcon?: boolean;
|
|
62
83
|
/**
|
|
63
84
|
* Custom upload button text
|
|
64
85
|
* @default "Choose files"
|
|
65
86
|
*/
|
|
66
|
-
buttonText?:
|
|
87
|
+
buttonText?: React.ReactNode;
|
|
67
88
|
/**
|
|
68
89
|
* Custom dropzone helper text
|
|
69
90
|
* @default "or drag and drop"
|
|
70
91
|
*/
|
|
71
|
-
helperText?:
|
|
92
|
+
helperText?: React.ReactNode;
|
|
72
93
|
/**
|
|
73
94
|
* Helper text displayed below the upload area
|
|
74
95
|
*/
|
|
75
|
-
description?:
|
|
96
|
+
description?: React.ReactNode;
|
|
76
97
|
/**
|
|
77
98
|
* Custom CSS class for the container
|
|
78
99
|
*/
|
|
@@ -96,11 +117,6 @@ export interface FileUploadThemeOverrides {
|
|
|
96
117
|
dropzoneStyle?: string;
|
|
97
118
|
dropzoneActiveStyle?: string;
|
|
98
119
|
dropzoneDisabledStyle?: string;
|
|
99
|
-
contentStyle?: string;
|
|
100
|
-
iconStyle?: string;
|
|
101
|
-
textStyle?: string;
|
|
102
|
-
helperTextStyle?: string;
|
|
103
|
-
descriptionStyle?: string;
|
|
104
120
|
previewContainerStyle?: string;
|
|
105
121
|
previewItemStyle?: string;
|
|
106
122
|
previewImageStyle?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import React__default, { useState, useEffect } from 'react';
|
|
3
3
|
import { useReducedMotion, motion, AnimatePresence } from 'motion/react';
|
|
4
|
-
import { DocumentIcon, TrashIcon,
|
|
4
|
+
import { DocumentIcon, TrashIcon, ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
5
5
|
import { Icon } from '../../core/Icon/Icon.js';
|
|
6
6
|
import { fileTypes } from '../../core/Icon/data/fileTypes.js';
|
|
7
7
|
import { cn } from '../../../utils/cn.js';
|
|
@@ -39,27 +39,38 @@ const RadialProgress = ({ progress, status, strokeWidth, trackColor, progressCol
|
|
|
39
39
|
setShowRemove(false);
|
|
40
40
|
}, [isComplete, completionDelay]);
|
|
41
41
|
const handleClick = () => {
|
|
42
|
-
if (
|
|
43
|
-
onRetry();
|
|
42
|
+
if (!onRemove)
|
|
44
43
|
return;
|
|
44
|
+
if (confirmRemove) {
|
|
45
|
+
setConfirmOpen(true);
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
setConfirmOpen(true);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
onRemove();
|
|
52
|
-
}
|
|
47
|
+
else {
|
|
48
|
+
onRemove();
|
|
53
49
|
}
|
|
54
50
|
};
|
|
55
51
|
const handleConfirm = () => {
|
|
56
52
|
setConfirmOpen(false);
|
|
57
53
|
onRemove === null || onRemove === void 0 ? void 0 : onRemove();
|
|
58
54
|
};
|
|
59
|
-
|
|
60
|
-
const
|
|
55
|
+
// Uploading/complete: the radial doubles as the cancel/remove target.
|
|
56
|
+
const isClickable = !!onRemove;
|
|
57
|
+
const ariaLabel = onRemove ? "Remove file" : undefined;
|
|
58
|
+
// Confirm popover shared by the radial (cancel/remove) and the error-state delete button.
|
|
59
|
+
const confirmPopover = (trigger, label, offsetValue) => (jsxs(Popover, { isOpen: confirmOpen, onOpenChange: (open) => {
|
|
60
|
+
setConfirmOpen(open);
|
|
61
|
+
if (!open)
|
|
62
|
+
setIsHovered(false);
|
|
63
|
+
}, placement: "bottom-end", offset: offsetValue, triggerType: "click", children: [jsx(Popover.Trigger, { children: trigger }), jsxs(Popover.Body, { className: "flex items-center p-1 border-[0.5px] rounded-xl gap-0.5 bg-opacity-50 backdrop-blur-md", children: [jsx("span", { className: "text-sm font-semibold text-[var(--color-text-secondary)] px-3", children: label }), jsxs("div", { className: "flex gap-1", children: [jsx(Button, { size: "sm", color: "danger", variant: "filled", onClick: handleConfirm, children: "Yes" }), jsx(Button, { size: "sm", variant: "soft", color: "neutral", onClick: () => setConfirmOpen(false), children: "No" })] })] })] }));
|
|
61
64
|
// Shared remove button used after completion tick
|
|
62
65
|
const removeButton = (jsx(motion.div, { initial: shouldReduceMotion ? false : REMOVE_BUTTON_ENTER.initial, animate: REMOVE_BUTTON_ENTER.animate, transition: REMOVE_BUTTON_ENTER.transition, className: "shrink-0", children: jsx(Button, { size: "sm", variant: "soft", color: "neutral", "aria-label": "Remove file", iconOnly: true, className: "rounded-full size-8", onClick: confirmRemove ? undefined : () => onRemove === null || onRemove === void 0 ? void 0 : onRemove(), children: jsx(TrashIcon, { className: "size-3.5", strokeWidth: 2 }) }) }, "remove-btn"));
|
|
66
|
+
// Error state: retry + delete buttons (no progress arc/percentage).
|
|
67
|
+
if (isError) {
|
|
68
|
+
const deleteButton = onRemove ? (jsx(Button, { size: "sm", variant: "soft", color: "neutral", "aria-label": "Remove file", iconOnly: true, className: "rounded-full size-8", onClick: confirmRemove ? undefined : () => onRemove(), children: jsx(TrashIcon, { className: "size-3.5", strokeWidth: 2 }) })) : null;
|
|
69
|
+
return (jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [onRetry && (jsx(Button, { size: "sm", variant: "soft", color: "neutral", "aria-label": "Retry upload", iconOnly: true, className: "rounded-full size-8", onClick: () => onRetry(), children: jsx(ArrowPathIcon, { className: "size-3.5", strokeWidth: 2 }) })), deleteButton &&
|
|
70
|
+
(confirmRemove
|
|
71
|
+
? confirmPopover(deleteButton, "Remove file?", 6)
|
|
72
|
+
: deleteButton)] }));
|
|
73
|
+
}
|
|
63
74
|
const radialElement = (jsxs("div", { className: cn("relative shrink-0", sizeClass, isClickable && "cursor-pointer"), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => {
|
|
64
75
|
if (!confirmOpen)
|
|
65
76
|
setIsHovered(false);
|
|
@@ -70,18 +81,14 @@ const RadialProgress = ({ progress, status, strokeWidth, trackColor, progressCol
|
|
|
70
81
|
handleClick();
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
|
-
: undefined, children: [jsxs("svg", { viewBox: `0 0 ${VIEWBOX} ${VIEWBOX}`, className: "w-full h-full -rotate-90", children: [jsx("circle", { cx: CENTER, cy: CENTER, r: radius, fill: "none", stroke: trackColor, strokeWidth: strokeWidth }), jsx(motion.circle, { cx: CENTER, cy: CENTER, r: radius, fill: "none", stroke: progressColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeDasharray: circumference, initial: { strokeDashoffset: circumference }, animate: { strokeDashoffset: isComplete ? 0 : offset }, transition: shouldReduceMotion ? { duration: 0 } : PROGRESS_SPRING })] }), jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsxs(AnimatePresence, { mode: "wait", initial: false, children: [isUploading && !isHovered && (jsx(motion.span, { className: centerTextStyle, ...(shouldReduceMotion ? {} : CROSSFADE), children: progress }, "percent")), isUploading && isHovered && onRemove && (jsx(motion.span, { ...(shouldReduceMotion ? {} : CROSSFADE), children: jsx(XMarkIcon, { className: "size-4 text-[var(--color-text-muted)]", strokeWidth: 2.5 }) }, "close")), isComplete && !showRemove && (jsx(motion.span, { ...(shouldReduceMotion ? {} : CROSSFADE), children: jsx("svg", { viewBox: "0 0 24 24", fill: "none", className: cn("size-4", statusIconColor), children: jsx("path", { d: "M5 13l4.5 4.5L19 7", stroke: "currentColor", strokeWidth: 3.5, strokeLinecap: "round", strokeLinejoin: "round" }) }) }, "check"))
|
|
84
|
+
: undefined, children: [jsxs("svg", { viewBox: `0 0 ${VIEWBOX} ${VIEWBOX}`, className: "w-full h-full -rotate-90", children: [jsx("circle", { cx: CENTER, cy: CENTER, r: radius, fill: "none", stroke: trackColor, strokeWidth: strokeWidth }), jsx(motion.circle, { cx: CENTER, cy: CENTER, r: radius, fill: "none", stroke: progressColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeDasharray: circumference, initial: { strokeDashoffset: circumference }, animate: { strokeDashoffset: isComplete ? 0 : offset }, transition: shouldReduceMotion ? { duration: 0 } : PROGRESS_SPRING })] }), jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsxs(AnimatePresence, { mode: "wait", initial: false, children: [isUploading && !isHovered && (jsx(motion.span, { className: centerTextStyle, ...(shouldReduceMotion ? {} : CROSSFADE), children: progress }, "percent")), isUploading && isHovered && onRemove && (jsx(motion.span, { ...(shouldReduceMotion ? {} : CROSSFADE), children: jsx(XMarkIcon, { className: "size-4 text-[var(--color-text-muted)]", strokeWidth: 2.5 }) }, "close")), isComplete && !showRemove && (jsx(motion.span, { ...(shouldReduceMotion ? {} : CROSSFADE), children: jsx("svg", { viewBox: "0 0 24 24", fill: "none", className: cn("size-4", statusIconColor), children: jsx("path", { d: "M5 13l4.5 4.5L19 7", stroke: "currentColor", strokeWidth: 3.5, strokeLinecap: "round", strokeLinejoin: "round" }) }) }, "check"))] }) })] }));
|
|
74
85
|
// Determine what to render: radial indicator or remove button
|
|
75
86
|
const showRemoveButton = isComplete && showRemove && onRemove;
|
|
76
87
|
const triggerElement = showRemoveButton ? removeButton : radialElement;
|
|
77
|
-
// No confirm popover needed — return the element directly
|
|
88
|
+
// No confirm popover needed — return the element directly.
|
|
78
89
|
if (!confirmRemove || !onRemove)
|
|
79
90
|
return triggerElement;
|
|
80
|
-
return (
|
|
81
|
-
setConfirmOpen(open);
|
|
82
|
-
if (!open)
|
|
83
|
-
setIsHovered(false);
|
|
84
|
-
}, placement: "bottom-end", offset: isComplete ? 2 : 6, triggerType: "click", children: [jsx(Popover.Trigger, { children: triggerElement }), jsxs(Popover.Body, { portal: false, className: "flex items-center p-0.5 border-[0.5px] rounded-xl gap-0.5", children: [jsx("span", { className: "text-base font-medium text-[var(--color-text-secondary)] px-3", children: isComplete ? "Remove file?" : "Cancel upload?" }), jsx(Button, { size: "sm", color: "danger", variant: "filled", onClick: handleConfirm, children: "Yes" }), jsx(Button, { size: "sm", variant: "soft", color: "neutral", onClick: () => setConfirmOpen(false), children: "No" })] })] }));
|
|
91
|
+
return confirmPopover(triggerElement, isComplete ? "Remove file?" : "Cancel upload?", isComplete ? 2 : 6);
|
|
85
92
|
};
|
|
86
93
|
// --- Main Component ---
|
|
87
94
|
const FileUploadProgress = React__default.forwardRef(({ items, onRemove, onRetry, showFileSize = true, confirmRemove = true, completionDelay = 1500, className, theme: themeOverrides, ...rest }, ref) => {
|
|
@@ -103,7 +110,7 @@ const FileUploadProgress = React__default.forwardRef(({ items, onRemove, onRetry
|
|
|
103
110
|
const clampedProgress = Math.min(100, Math.max(0, item.progress));
|
|
104
111
|
const ext = getFileExtension(item.file.name);
|
|
105
112
|
const hasFileTypeIcon = ext !== null && isKnownFileType(ext);
|
|
106
|
-
return (jsxs("div", { className: (_a = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.itemStyle) !== null && _a !== void 0 ? _a : t.itemStyle, children: [hasFileTypeIcon ? (jsx("span", { className: "shrink-0", children: jsx(Icon, { icon: ext, size: "lg" }) })) : (jsx("span", { className: cn((_b = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.iconContainerStyle) !== null && _b !== void 0 ? _b : t.iconContainerStyle, t.iconSize), children: jsx(DocumentIcon, { className: (_c = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.iconStyle) !== null && _c !== void 0 ? _c : t.iconStyle }) })), jsxs("div", { className: (_d = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.fileInfoStyle) !== null && _d !== void 0 ? _d : t.fileInfoStyle, children: [jsx("p", { className: (_e = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.fileNameStyle) !== null && _e !== void 0 ? _e : t.fileNameStyle, children: item.file.name }),
|
|
113
|
+
return (jsxs("div", { className: (_a = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.itemStyle) !== null && _a !== void 0 ? _a : t.itemStyle, children: [hasFileTypeIcon ? (jsx("span", { className: "shrink-0", children: jsx(Icon, { icon: ext, size: "lg" }) })) : (jsx("span", { className: cn((_b = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.iconContainerStyle) !== null && _b !== void 0 ? _b : t.iconContainerStyle, t.iconSize), children: jsx(DocumentIcon, { className: (_c = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.iconStyle) !== null && _c !== void 0 ? _c : t.iconStyle }) })), jsxs("div", { className: (_d = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.fileInfoStyle) !== null && _d !== void 0 ? _d : t.fileInfoStyle, children: [jsx("p", { className: (_e = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.fileNameStyle) !== null && _e !== void 0 ? _e : t.fileNameStyle, children: item.file.name }), item.status === "error" && item.error ? (jsx("p", { className: (_f = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.errorTextStyle) !== null && _f !== void 0 ? _f : t.errorTextStyle, children: item.error })) : (showFileSize && (jsx("p", { className: (_g = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.fileSizeStyle) !== null && _g !== void 0 ? _g : t.fileSizeStyle, children: formatFileSize(item.file.size) })))] }), jsx(RadialProgress, { progress: clampedProgress, status: item.status, strokeWidth: t.radialStroke, trackColor: (_h = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.radialTrackColor) !== null && _h !== void 0 ? _h : t.radialTrackColor, progressColor: radialProgressColors[item.status], centerTextStyle: (_j = themeOverrides === null || themeOverrides === void 0 ? void 0 : themeOverrides.radialCenterTextStyle) !== null && _j !== void 0 ? _j : t.radialCenterTextStyle, statusIconColor: statusIconColors[item.status], sizeClass: t.radialSize, shouldReduceMotion: shouldReduceMotion, confirmRemove: confirmRemove, completionDelay: completionDelay, onRemove: onRemove ? () => onRemove(item.id) : undefined, onRetry: onRetry ? () => onRetry(item.id) : undefined })] }, item.id));
|
|
107
114
|
}) }));
|
|
108
115
|
});
|
|
109
116
|
FileUploadProgress.displayName = "FileUploadProgress";
|
|
@@ -6,13 +6,13 @@ const fileUploadProgressTheme = {
|
|
|
6
6
|
fileInfoStyle: "flex-1 min-w-0",
|
|
7
7
|
fileNameStyle: "text-sm font-medium text-[var(--color-text-primary)] truncate",
|
|
8
8
|
fileSizeStyle: "text-xs text-[var(--color-text-muted)]",
|
|
9
|
-
radialTrackColor: "var(--color-
|
|
9
|
+
radialTrackColor: "var(--color-surface-sunken)",
|
|
10
10
|
radialProgressColors: {
|
|
11
11
|
uploading: "var(--color-primary)",
|
|
12
12
|
complete: "var(--color-success)",
|
|
13
13
|
error: "var(--color-danger)",
|
|
14
14
|
},
|
|
15
|
-
radialCenterTextStyle: "text-
|
|
15
|
+
radialCenterTextStyle: "text-xs tracking-tight font-semibold tabular-nums text-[var(--color-text-secondary)]",
|
|
16
16
|
statusIconColors: {
|
|
17
17
|
uploading: "",
|
|
18
18
|
complete: "text-[var(--color-success)]",
|
|
@@ -8,17 +8,17 @@ const inputTheme = {
|
|
|
8
8
|
// Base container styles
|
|
9
9
|
wrapperStyle: formsBaseTheme.wrapperStyle,
|
|
10
10
|
// Input group styles - no focus-within (focus is managed via JS for contentStart/contentEnd compatibility)
|
|
11
|
-
inputGroupStyle: "relative flex rounded-[var(--form-radius)]
|
|
12
|
-
"bg-
|
|
13
|
-
|
|
11
|
+
inputGroupStyle: "relative flex rounded-[var(--form-radius)] shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
12
|
+
"bg-[var(--color-surface)] transition-all " +
|
|
13
|
+
// Hover edge: strengthen the border to --color-border-secondary on hover. Scoped with
|
|
14
|
+
// not-focus-within so the JS-driven focus border (primary) always wins.
|
|
15
|
+
"not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-border-secondary)]",
|
|
14
16
|
// JS-driven focus ring (applied when isFocused is true)
|
|
15
|
-
inputGroupFocusStyle: "
|
|
16
|
-
"
|
|
17
|
-
"dark:outline-[var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20",
|
|
17
|
+
inputGroupFocusStyle: "shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10 " +
|
|
18
|
+
"dark:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20",
|
|
18
19
|
// JS-driven focus ring for invalid state
|
|
19
|
-
inputGroupFocusInvalidStyle: "
|
|
20
|
-
"
|
|
21
|
-
"dark:outline-[var(--color-danger-500)] dark:ring-[var(--color-danger-500)]/20",
|
|
20
|
+
inputGroupFocusInvalidStyle: "shadow-[inset_0_0_0_1px_var(--color-danger)] ring-4 ring-[var(--color-danger)]/10 " +
|
|
21
|
+
"dark:shadow-[inset_0_0_0_1px_var(--color-danger-500)] dark:ring-[var(--color-danger-500)]/20",
|
|
22
22
|
// Base input styles - extends common form control with design tokens
|
|
23
23
|
baseStyle: `${formsBaseTheme.controlBaseStyle} py-2 px-3`,
|
|
24
24
|
// Size variants - composed from atomic tokens
|
|
@@ -31,8 +31,11 @@ const inputTheme = {
|
|
|
31
31
|
states: {
|
|
32
32
|
default: formsBaseTheme.states.default,
|
|
33
33
|
disabled: formsBaseTheme.states.disabled,
|
|
34
|
-
invalid: "
|
|
35
|
-
|
|
34
|
+
invalid: "shadow-[inset_0_0_0_1px_var(--color-danger)] bg-[var(--color-danger-50)] " +
|
|
35
|
+
// Keep the danger border on hover (don't let the hover edge override it)
|
|
36
|
+
"not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-danger)] " +
|
|
37
|
+
"dark:bg-[var(--color-danger-950)]/30 dark:shadow-[inset_0_0_0_1px_var(--color-danger-500)] " +
|
|
38
|
+
"dark:not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-danger-500)]",
|
|
36
39
|
},
|
|
37
40
|
// Helper text styles with state handling
|
|
38
41
|
helperText: formsBaseTheme.helperText,
|
|
@@ -67,10 +70,8 @@ const inputTheme = {
|
|
|
67
70
|
// Number input controls
|
|
68
71
|
numberButtonsContainerStyle: "absolute inset-y-0 right-0 flex flex-row items-center pr-2 z-10 gap-1",
|
|
69
72
|
numberButtonStyle: "bg-[var(--color-background-secondary)] flex items-center justify-center " +
|
|
70
|
-
"text-[var(--color-text-muted)] hover:bg-[var(--color-
|
|
71
|
-
"hover:text-[var(--color-text-primary)] cursor-pointer size-6 rounded-sm transition-colors
|
|
72
|
-
"dark:bg-[var(--color-neutral-800)] dark:text-[var(--color-neutral-500)] " +
|
|
73
|
-
"dark:hover:bg-[var(--color-neutral-700)] dark:hover:text-[var(--color-neutral-200)]",
|
|
73
|
+
"text-[var(--color-text-muted)] hover:bg-[var(--color-surface-hover)] " +
|
|
74
|
+
"hover:text-[var(--color-text-primary)] cursor-pointer size-6 rounded-sm transition-colors",
|
|
74
75
|
// Type-specific styles organized by input type
|
|
75
76
|
typeStyles: {
|
|
76
77
|
password: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Follows Flikkui design system with CSS variables
|
|
4
4
|
*/
|
|
5
5
|
const inputCounterTheme = {
|
|
6
|
-
baseStyle: "flex items-center gap-3 transition-all p-1 border border-[var(--color-border)] rounded-[var(--form-radius)] w-fit",
|
|
6
|
+
baseStyle: "flex items-center gap-3 transition-all p-1 bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--form-radius)] w-fit",
|
|
7
7
|
buttonWrapperStyle: "flex-shrink-0",
|
|
8
8
|
numberStyle: "min-w-fit text-center text-sm font-medium text-[var(--color-text-primary)] tabular-nums select-none",
|
|
9
9
|
};
|