@flikk/ui 1.0.0-beta.26 → 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/PromptInput/PromptInput.js +3 -0
- package/dist/components/ai/PromptInput/VoiceRecorder.js +3 -0
- package/dist/components/core/Button/Button.theme.js +1 -1
- package/dist/components/core/Calendar/Calendar.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/Dropdown/Dropdown.theme.js +2 -3
- package/dist/components/core/Dropdown/DropdownItem.js +1 -1
- package/dist/components/core/Dropdown/DropdownMenu.js +1 -1
- 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/NavItem/NavItem.js +20 -4
- package/dist/components/core/NavItem/NavItem.theme.js +13 -2
- package/dist/components/core/Popover/Popover.types.d.ts +6 -2
- package/dist/components/core/Popover/PopoverBody.js +10 -3
- package/dist/components/core/Segmented/Segmented.theme.js +1 -1
- package/dist/components/core/Sidebar/Sidebar.d.ts +2 -0
- package/dist/components/core/Sidebar/Sidebar.js +15 -7
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +92 -4
- 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 +5 -1
- package/dist/components/core/Sidebar/SidebarContext.js +1 -1
- package/dist/components/core/Sidebar/SidebarHeader.js +10 -2
- package/dist/components/core/Sidebar/SidebarLogo.js +38 -3
- 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 +1 -1
- package/dist/components/core/Sidebar/SidebarUserProfile.js +2 -2
- package/dist/components/core/Sidebar/index.d.ts +3 -1
- package/dist/components/core/Tag/Tag.js +29 -5
- package/dist/components/core/Tag/Tag.theme.js +13 -8
- 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 +1 -0
- package/dist/components/core/index.js +4 -0
- 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/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/Table/Table.theme.js +3 -5
- package/dist/components/effects/Aurora/Aurora.d.ts +6 -17
- package/dist/components/effects/Aurora/Aurora.js +297 -131
- package/dist/components/effects/Aurora/Aurora.types.d.ts +6 -0
- 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 +14 -23
- 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 +6 -13
- 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 +87 -48
- 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/Input/Input.theme.js +13 -10
- 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/RichTextEditor/RichTextEditor.js +3 -3
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +5 -5
- package/dist/components/forms/Select/Select.js +2 -2
- package/dist/components/forms/Select/Select.theme.js +3 -3
- package/dist/components/forms/TimePicker/TimePickerContent.js +3 -0
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +1 -1
- package/dist/components/forms/forms.theme.js +21 -16
- package/dist/components/forms/index.js +3 -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 +3 -0
- 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 +6 -0
- 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 +51 -12
|
@@ -10,6 +10,12 @@ export interface AuroraProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
10
10
|
opacity?: number;
|
|
11
11
|
/** Blob size multiplier. Higher values = larger blobs (default: 1) */
|
|
12
12
|
size?: number;
|
|
13
|
+
/** CSS mix-blend-mode for the aurora blobs (default: 'screen') */
|
|
14
|
+
blendMode?: 'screen' | 'color-dodge' | 'overlay' | 'hard-light' | 'normal' | 'plus-lighter';
|
|
15
|
+
/** Whether to render a premium stardust film-grain noise overlay on top (default: true) */
|
|
16
|
+
showNoise?: boolean;
|
|
17
|
+
/** Opacity of the noise overlay, 0 to 1 (default: 0.06) */
|
|
18
|
+
noiseOpacity?: number;
|
|
13
19
|
/** Additional className for the container */
|
|
14
20
|
className?: string;
|
|
15
21
|
}
|
|
@@ -100,7 +100,7 @@ const ComboboxOptions = ({ isOpen, onClose, triggerRef, portal, placement, offse
|
|
|
100
100
|
}, children: isCreateOption ? (jsxs("span", { className: "flex items-center gap-1", children: [jsx(PlusIcon, { className: "size-4 shrink-0", strokeWidth: 2 }), jsx("span", { className: "block truncate", children: option.label })] })) : (jsx("span", { className: "block truncate", children: option.label })) }, option.id));
|
|
101
101
|
}), filteredOptions.length > 100 && (jsxs("div", { className: "px-3 py-2 text-xs text-[var(--color-text-muted)] text-center border-t border-[var(--color-border)]", children: ["Showing 100 of ", filteredOptions.length, " results \u2014 refine your search"] }))] })) })) }));
|
|
102
102
|
if (portal && isClient) {
|
|
103
|
-
return createPortal(jsx("div", { className: isDarkMode ? "dark" : undefined, children: optionsContent }), document.body);
|
|
103
|
+
return createPortal(jsx("div", { "data-flikkui-portal": "", className: isDarkMode ? "dark" : undefined, children: optionsContent }), document.body);
|
|
104
104
|
}
|
|
105
105
|
return optionsContent;
|
|
106
106
|
};
|
|
@@ -28,14 +28,12 @@ const comboboxTheme = {
|
|
|
28
28
|
focusStates: formsBaseTheme.focusStates,
|
|
29
29
|
// Icon positioning styles
|
|
30
30
|
iconStartStyle: formsBaseTheme.iconStyles.left,
|
|
31
|
-
iconEndStyle: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-[var(--color-text-muted)]
|
|
32
|
-
"dark:text-[var(--color-neutral-500)]",
|
|
31
|
+
iconEndStyle: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-[var(--color-text-muted)]",
|
|
33
32
|
// Icon padding configuration
|
|
34
33
|
iconPadding: formsBaseTheme.iconStyles.padding,
|
|
35
34
|
// Clear button styles
|
|
36
35
|
clearButtonStyle: "absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer " +
|
|
37
|
-
"text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors z-10
|
|
38
|
-
"dark:text-[var(--color-neutral-500)] dark:hover:text-[var(--color-neutral-200)]",
|
|
36
|
+
"text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors z-10",
|
|
39
37
|
clearIconStyle: "size-4",
|
|
40
38
|
// Dropdown styles - matches Select component for consistency
|
|
41
39
|
dropdownStyle: formsBaseTheme.dropdownStyles.container + " py-1",
|
|
@@ -104,7 +104,7 @@ const CronInput = React__default.forwardRef(({ value: controlledValue, defaultVa
|
|
|
104
104
|
const showVisual = mode === "visual" || (mode === "both" && activeMode === "visual");
|
|
105
105
|
const showRaw = mode === "raw" || (mode === "both" && activeMode === "raw");
|
|
106
106
|
const helperId = `${generatedId}-helper`;
|
|
107
|
-
return (jsxs("div", { ref: ref, className: cn(theme.baseStyle, className), role: "group", "aria-label": label || "Cron expression builder", ...props, children: [label && (jsx(FormLabel, { htmlFor: `${generatedId}-cron`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", children: label })), mode === "both" && (jsxs("div", { className: "flex items-center gap-1.5", children: [jsxs("button", { type: "button", onClick: () => setActiveMode("visual"), disabled: isDisabled, className: cn(theme.modeToggleStyle, activeMode === "visual" && theme.modeToggleActiveStyle), "aria-pressed": activeMode === "visual", children: [jsx(CalendarIcon, { className: "w-3.5 h-3.5 mr-1", "aria-hidden": "true" }), "Visual"] }), jsxs("button", { type: "button", onClick: () => setActiveMode("raw"), disabled: isDisabled, className: cn(theme.modeToggleStyle, activeMode === "raw" && theme.modeToggleActiveStyle), "aria-pressed": activeMode === "raw", children: [jsx(ClockIcon, { className: "w-3.5 h-3.5 mr-1", "aria-hidden": "true" }), "Cron"] })] })), showPresets && (jsxs("div", { children: [jsx("div", { className: cn(theme.sectionHeadingStyle, "mb-1.5"), children: "Presets" }), jsx("div", { className: "flex flex-wrap gap-1.5", children: CRON_PRESETS.map((preset) => (jsx("button", { type: "button", onClick: () => handlePresetClick(preset.value), disabled: isDisabled, className: cn(theme.presetButtonStyle, activePreset === preset.value && theme.presetButtonActiveStyle), title: preset.description, "aria-pressed": activePreset === preset.value, children: preset.label }, preset.value))) })] })), showVisual && (jsxs("div", { className: cn(theme.builderStyle, isInvalid && "ring-1 ring-[var(--color-danger)] rounded-[var(--form-radius)]"), children: [jsx(FieldSelect, { label: "Minute", value: fields.minute, options: minuteOptions, onChange: (v) => handleFieldChange("minute", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Hour", value: fields.hour, options: hourOptions, onChange: (v) => handleFieldChange("hour", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Day of Month", value: fields.dayOfMonth, options: dayOfMonthOptions, onChange: (v) => handleFieldChange("dayOfMonth", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Month", value: fields.month, options: monthOptions, onChange: (v) => handleFieldChange("month", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Day of Week", value: fields.dayOfWeek, options: dayOfWeekOptions, onChange: (v) => handleFieldChange("dayOfWeek", v), disabled: isDisabled, size: size })] })), showRaw && (jsxs("div", { children: [jsx("div", { className: cn(theme.sectionHeadingStyle, "mb-1.5"), children: "Cron Expression" }), jsx("input", { type: "text", value: rawInput, onChange: handleRawInputChange, onBlur: handleRawInputBlur, onKeyDown: handleRawKeyDown, disabled: isDisabled, placeholder: "* * * * *", className: cn(theme.rawInputStyle, theme.rawInputSizes[size], isInvalid && "border-[var(--color-danger)] focus:border-[var(--color-danger)] focus:ring-[var(--color-danger)]"), id: `${generatedId}-cron`, "aria-label": "Cron expression", "aria-invalid": isInvalid || !validateCron(rawInput), "aria-describedby": helperText ? helperId : undefined, spellCheck: false, autoComplete: "off" }), !validateCron(rawInput) && rawInput !== cronValue && (jsx("div", { className: cn(theme.errorStyle, "mt-1"), children: "Invalid cron expression. Format: minute hour day month weekday" }))] })), showVisual && !showRaw && (jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: cn(theme.fieldLabelStyle, "shrink-0"), children: "Expression:" }), jsx("code", { className: cn("font-mono text-sm px-2 py-1 rounded-[var(--form-radius)]", "bg-[var(--color-
|
|
107
|
+
return (jsxs("div", { ref: ref, className: cn(theme.baseStyle, className), role: "group", "aria-label": label || "Cron expression builder", ...props, children: [label && (jsx(FormLabel, { htmlFor: `${generatedId}-cron`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", children: label })), mode === "both" && (jsxs("div", { className: "flex items-center gap-1.5", children: [jsxs("button", { type: "button", onClick: () => setActiveMode("visual"), disabled: isDisabled, className: cn(theme.modeToggleStyle, activeMode === "visual" && theme.modeToggleActiveStyle), "aria-pressed": activeMode === "visual", children: [jsx(CalendarIcon, { className: "w-3.5 h-3.5 mr-1", "aria-hidden": "true" }), "Visual"] }), jsxs("button", { type: "button", onClick: () => setActiveMode("raw"), disabled: isDisabled, className: cn(theme.modeToggleStyle, activeMode === "raw" && theme.modeToggleActiveStyle), "aria-pressed": activeMode === "raw", children: [jsx(ClockIcon, { className: "w-3.5 h-3.5 mr-1", "aria-hidden": "true" }), "Cron"] })] })), showPresets && (jsxs("div", { children: [jsx("div", { className: cn(theme.sectionHeadingStyle, "mb-1.5"), children: "Presets" }), jsx("div", { className: "flex flex-wrap gap-1.5", children: CRON_PRESETS.map((preset) => (jsx("button", { type: "button", onClick: () => handlePresetClick(preset.value), disabled: isDisabled, className: cn(theme.presetButtonStyle, activePreset === preset.value && theme.presetButtonActiveStyle), title: preset.description, "aria-pressed": activePreset === preset.value, children: preset.label }, preset.value))) })] })), showVisual && (jsxs("div", { className: cn(theme.builderStyle, isInvalid && "ring-1 ring-[var(--color-danger)] rounded-[var(--form-radius)]"), children: [jsx(FieldSelect, { label: "Minute", value: fields.minute, options: minuteOptions, onChange: (v) => handleFieldChange("minute", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Hour", value: fields.hour, options: hourOptions, onChange: (v) => handleFieldChange("hour", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Day of Month", value: fields.dayOfMonth, options: dayOfMonthOptions, onChange: (v) => handleFieldChange("dayOfMonth", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Month", value: fields.month, options: monthOptions, onChange: (v) => handleFieldChange("month", v), disabled: isDisabled, size: size }), jsx(FieldSelect, { label: "Day of Week", value: fields.dayOfWeek, options: dayOfWeekOptions, onChange: (v) => handleFieldChange("dayOfWeek", v), disabled: isDisabled, size: size })] })), showRaw && (jsxs("div", { children: [jsx("div", { className: cn(theme.sectionHeadingStyle, "mb-1.5"), children: "Cron Expression" }), jsx("input", { type: "text", value: rawInput, onChange: handleRawInputChange, onBlur: handleRawInputBlur, onKeyDown: handleRawKeyDown, disabled: isDisabled, placeholder: "* * * * *", className: cn(theme.rawInputStyle, theme.rawInputSizes[size], isInvalid && "border-[var(--color-danger)] focus:border-[var(--color-danger)] focus:ring-[var(--color-danger)]"), id: `${generatedId}-cron`, "aria-label": "Cron expression", "aria-invalid": isInvalid || !validateCron(rawInput), "aria-describedby": helperText ? helperId : undefined, spellCheck: false, autoComplete: "off" }), !validateCron(rawInput) && rawInput !== cronValue && (jsx("div", { className: cn(theme.errorStyle, "mt-1"), children: "Invalid cron expression. Format: minute hour day month weekday" }))] })), showVisual && !showRaw && (jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: cn(theme.fieldLabelStyle, "shrink-0"), children: "Expression:" }), jsx("code", { className: cn("font-mono text-sm px-2 py-1 rounded-[var(--form-radius)]", "bg-[var(--color-surface-sunken)] text-[var(--color-text-primary)]"), children: cronValue })] })), showDescription && isValid && (jsxs("div", { className: theme.descriptionStyle, children: [jsx(ClockIcon, { className: "w-4 h-4 shrink-0", "aria-hidden": "true" }), jsx("span", { children: description })] })), showNextRuns && isValid && nextRuns.length > 0 && (jsxs("div", { className: theme.nextRunsStyle, children: [jsx("div", { className: cn(theme.sectionHeadingStyle, "mb-1"), children: "Next runs" }), nextRuns.map((date, index) => (jsxs("div", { className: theme.nextRunItemStyle, children: [jsx(CalendarIcon, { className: "w-3.5 h-3.5 shrink-0", "aria-hidden": "true" }), jsx("span", { children: formatNextRun(date) })] }, index)))] })), helperText && (jsx("div", { id: helperId, className: cn("text-sm mt-1", isInvalid
|
|
108
108
|
? "text-[var(--color-danger)]"
|
|
109
109
|
: "text-[var(--color-text-muted)]"), children: helperText }))] }));
|
|
110
110
|
});
|
|
@@ -8,21 +8,20 @@ const cronInputTheme = {
|
|
|
8
8
|
// Outer wrapper
|
|
9
9
|
baseStyle: "relative w-full flex flex-col gap-3",
|
|
10
10
|
// Label
|
|
11
|
-
labelStyle: "text-sm font-medium text-[var(--color-text-primary)]
|
|
11
|
+
labelStyle: "text-sm font-medium text-[var(--color-text-primary)]",
|
|
12
12
|
// Visual builder grid
|
|
13
13
|
builderStyle: "grid grid-cols-5 gap-2",
|
|
14
14
|
// Individual field group
|
|
15
15
|
fieldGroupStyle: "flex flex-col gap-1",
|
|
16
16
|
// Field label
|
|
17
|
-
fieldLabelStyle: "text-xs font-medium text-[var(--color-text-secondary)]
|
|
17
|
+
fieldLabelStyle: "text-xs font-medium text-[var(--color-text-secondary)]",
|
|
18
18
|
// Select dropdown
|
|
19
19
|
fieldSelectStyle: "w-full rounded-[var(--form-radius)] outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
20
|
-
"bg-
|
|
21
|
-
"focus:shadow-[
|
|
20
|
+
"bg-[var(--color-surface)] text-[var(--color-text-primary)] cursor-pointer transition-all " +
|
|
21
|
+
"focus:shadow-[inset_0_0_0_1px_var(--color-primary-600)] " +
|
|
22
22
|
"focus:ring-4 focus:ring-[var(--color-primary)]/10 " +
|
|
23
23
|
"disabled:cursor-not-allowed disabled:bg-[var(--color-background-disabled)] disabled:text-[var(--color-text-disabled)] " +
|
|
24
|
-
"dark:
|
|
25
|
-
"dark:focus:shadow-[inset_0_0_0_2px_var(--color-primary-500)] dark:focus:ring-[var(--color-primary-500)]/20",
|
|
24
|
+
"dark:focus:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:focus:ring-[var(--color-primary-500)]/20",
|
|
26
25
|
// Size variants for field selects
|
|
27
26
|
fieldSelectSizes: {
|
|
28
27
|
sm: `${formsBaseTheme.sizes.sm.text} ${formsBaseTheme.sizes.sm.padding} ${formsBaseTheme.sizes.sm.height}`,
|
|
@@ -31,13 +30,12 @@ const cronInputTheme = {
|
|
|
31
30
|
},
|
|
32
31
|
// Raw input field
|
|
33
32
|
rawInputStyle: "w-full rounded-[var(--form-radius)] outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
34
|
-
"bg-
|
|
33
|
+
"bg-[var(--color-surface)] text-[var(--color-text-primary)] font-mono transition-all " +
|
|
35
34
|
"placeholder:text-[var(--color-text-placeholder)] focus:outline-none " +
|
|
36
|
-
"focus:shadow-[
|
|
35
|
+
"focus:shadow-[inset_0_0_0_1px_var(--color-primary-600)] " +
|
|
37
36
|
"focus:ring-4 focus:ring-[var(--color-primary)]/10 " +
|
|
38
37
|
"disabled:cursor-not-allowed disabled:bg-[var(--color-background-disabled)] disabled:text-[var(--color-text-disabled)] " +
|
|
39
|
-
"dark:
|
|
40
|
-
"dark:focus:shadow-[inset_0_0_0_2px_var(--color-primary-500)] dark:focus:ring-[var(--color-primary-500)]/20",
|
|
38
|
+
"dark:focus:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:focus:ring-[var(--color-primary-500)]/20",
|
|
41
39
|
// Size variants for raw input
|
|
42
40
|
rawInputSizes: {
|
|
43
41
|
sm: `${formsBaseTheme.sizes.sm.text} ${formsBaseTheme.sizes.sm.padding} ${formsBaseTheme.sizes.sm.height}`,
|
|
@@ -47,9 +45,7 @@ const cronInputTheme = {
|
|
|
47
45
|
// Mode toggle button
|
|
48
46
|
modeToggleStyle: "inline-flex items-center justify-center rounded-[var(--form-radius)] px-3 py-1.5 text-xs font-medium " +
|
|
49
47
|
"cursor-pointer transition-all outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
50
|
-
"text-[var(--color-text-secondary)] bg-
|
|
51
|
-
"dark:bg-[var(--color-neutral-900)] dark:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] dark:text-[var(--color-neutral-400)] " +
|
|
52
|
-
"dark:hover:bg-[var(--color-neutral-800)]",
|
|
48
|
+
"text-[var(--color-text-secondary)] bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)]",
|
|
53
49
|
// Active mode toggle
|
|
54
50
|
modeToggleActiveStyle: "shadow-[inset_0_0_0_1px_var(--color-primary)] bg-[var(--color-primary-50)] text-[var(--color-primary)] " +
|
|
55
51
|
"hover:bg-[var(--color-primary-50)] " +
|
|
@@ -58,25 +54,20 @@ const cronInputTheme = {
|
|
|
58
54
|
// Preset button
|
|
59
55
|
presetButtonStyle: "inline-flex items-center rounded-[var(--form-radius)] px-2.5 py-1 text-xs font-medium " +
|
|
60
56
|
"cursor-pointer transition-all outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
61
|
-
"text-[var(--color-text-secondary)] bg-
|
|
62
|
-
"hover:text-[var(--color-text-primary)]
|
|
63
|
-
"dark:bg-[var(--color-neutral-900)] dark:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] dark:text-[var(--color-neutral-400)] " +
|
|
64
|
-
"dark:hover:bg-[var(--color-neutral-800)] dark:hover:text-[var(--color-neutral-200)]",
|
|
57
|
+
"text-[var(--color-text-secondary)] bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)] " +
|
|
58
|
+
"hover:text-[var(--color-text-primary)]",
|
|
65
59
|
// Active preset button
|
|
66
60
|
presetButtonActiveStyle: "shadow-[inset_0_0_0_1px_var(--color-primary)] bg-[var(--color-primary-50)] text-[var(--color-primary)] " +
|
|
67
61
|
"hover:bg-[var(--color-primary-100)] " +
|
|
68
62
|
"dark:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:bg-[var(--color-primary-500)]/10 dark:text-[var(--color-primary-400)]",
|
|
69
63
|
// Human-readable description
|
|
70
|
-
descriptionStyle: "flex items-center gap-2 text-sm text-[var(--color-text-muted)]
|
|
71
|
-
"dark:text-[var(--color-neutral-400)]",
|
|
64
|
+
descriptionStyle: "flex items-center gap-2 text-sm text-[var(--color-text-muted)]",
|
|
72
65
|
// Next runs container
|
|
73
66
|
nextRunsStyle: "flex flex-col gap-1",
|
|
74
67
|
// Individual next run item
|
|
75
|
-
nextRunItemStyle: "flex items-center gap-2 text-xs text-[var(--color-text-muted)]
|
|
76
|
-
"dark:text-[var(--color-neutral-400)]",
|
|
68
|
+
nextRunItemStyle: "flex items-center gap-2 text-xs text-[var(--color-text-muted)]",
|
|
77
69
|
// Section heading
|
|
78
|
-
sectionHeadingStyle: "text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide
|
|
79
|
-
"dark:text-[var(--color-neutral-400)]",
|
|
70
|
+
sectionHeadingStyle: "text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide",
|
|
80
71
|
// Validation error
|
|
81
72
|
errorStyle: "text-xs text-[var(--color-danger)] dark:text-[var(--color-danger-400)]",
|
|
82
73
|
};
|
|
@@ -10,8 +10,8 @@ import { formsBaseTheme } from '../forms.theme.js';
|
|
|
10
10
|
const datePickerTheme = {
|
|
11
11
|
containerStyle: formsBaseTheme.wrapperStyle,
|
|
12
12
|
baseStyle: formsBaseTheme.inputGroupBaseStyle + " justify-between cursor-pointer",
|
|
13
|
-
// Trigger button style -
|
|
14
|
-
triggerButtonStyle: "w-full cursor-pointer bg-
|
|
13
|
+
// Trigger button style - surface flips automatically between light/dark
|
|
14
|
+
triggerButtonStyle: "w-full cursor-pointer bg-[var(--color-surface)]",
|
|
15
15
|
// Placeholder text style - matches Input placeholder styling
|
|
16
16
|
placeholderTextStyle: "text-[var(--color-text-placeholder)]",
|
|
17
17
|
// Selected value text style
|
|
@@ -26,23 +26,16 @@ const datePickerTheme = {
|
|
|
26
26
|
triggerFocusStates: formsBaseTheme.focusStates,
|
|
27
27
|
iconEndStyle: formsBaseTheme.iconStyles.right,
|
|
28
28
|
iconEndPadding: formsBaseTheme.iconStyles.padding.right,
|
|
29
|
-
// Dropdown body -
|
|
30
|
-
bodyStyle: "min-w-fit max-w-none bg-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
presetsContainerStyle: "flex flex-col gap-1 p-3 border-b sm:border-b-0 sm:border-r border-[var(--color-border)] bg-[var(--color-neutral-50)] sm:min-w-[180px] " +
|
|
36
|
-
"dark:bg-[var(--color-neutral-800)] dark:border-[var(--color-neutral-700)]",
|
|
37
|
-
// Preset button - with dark mode support
|
|
29
|
+
// Dropdown body - floating overlay surface flips automatically
|
|
30
|
+
bodyStyle: "min-w-fit max-w-none bg-[var(--color-surface-overlay)] border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-real-xl p-0 overflow-hidden z-[1000] fixed",
|
|
31
|
+
bodyWithPresetsStyle: "flex flex-col sm:flex-row w-fit max-w-[calc(100vw-2rem)] sm:max-w-none bg-[var(--color-surface-overlay)] border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-lg p-0 overflow-hidden z-[1000] fixed",
|
|
32
|
+
// Presets sidebar - recessed sunken panel flips automatically
|
|
33
|
+
presetsContainerStyle: "flex flex-col gap-1 p-3 border-b sm:border-b-0 sm:border-r border-[var(--color-border)] bg-[var(--color-surface-sunken)] sm:min-w-[180px]",
|
|
34
|
+
// Preset button - surface hover/active fills flip automatically
|
|
38
35
|
presetButtonStyle: "w-full text-left px-3 py-2 text-sm rounded-[calc(var(--radius-base)*0.75)] transition-colors cursor-pointer " +
|
|
39
|
-
"text-[var(--color-text-secondary)] hover:bg-
|
|
40
|
-
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 " +
|
|
41
|
-
"data-[active=true]:bg-
|
|
42
|
-
// Dark mode
|
|
43
|
-
"dark:text-[var(--color-neutral-400)] dark:hover:bg-[var(--color-neutral-700)] dark:hover:text-[var(--color-neutral-200)] " +
|
|
44
|
-
"dark:data-[active=true]:bg-[var(--color-neutral-700)] dark:data-[active=true]:text-[var(--color-neutral-100)] " +
|
|
45
|
-
"dark:focus-visible:ring-offset-[var(--color-neutral-800)]",
|
|
36
|
+
"text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] " +
|
|
37
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-sunken)] " +
|
|
38
|
+
"data-[active=true]:bg-[var(--color-surface)] data-[active=true]:text-[var(--color-text-primary)] data-[active=true]:font-medium",
|
|
46
39
|
helperTextStyle: formsBaseTheme.helperText,
|
|
47
40
|
helperTextStates: {
|
|
48
41
|
default: "text-[var(--color-text-muted)] dark:text-[var(--color-neutral-400)]",
|
|
@@ -26,7 +26,7 @@ const DatePickerTrigger = ({ id, disabled, isOpen, state = "default", size = "md
|
|
|
26
26
|
// Apply focused styles when open - same pattern as SelectButton
|
|
27
27
|
const activeFocusState = isOpen
|
|
28
28
|
? focusStateStyle ||
|
|
29
|
-
"shadow-[
|
|
29
|
+
"shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10"
|
|
30
30
|
: "";
|
|
31
31
|
return (jsx("div", { className: "relative w-full", children: jsxs("button", { ref: triggerRef, id: id, type: "button", className: cn(baseStyle, sizeStyle, stateStyle, hoverStateStyle, activeFocusState, iconEndPadding, triggerButtonStyle, "text-left", className), onClick: onClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-label": "Open calendar", "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, children: [children, jsx("span", { className: iconEndStyle, children: jsx(CalendarIcon, { className: "h-4 w-4" }) })] }) }));
|
|
32
32
|
};
|
|
@@ -6,7 +6,7 @@ import { formsBaseTheme } from '../forms.theme.js';
|
|
|
6
6
|
const dateRangePickerTheme = {
|
|
7
7
|
containerStyle: formsBaseTheme.wrapperStyle,
|
|
8
8
|
baseStyle: formsBaseTheme.inputGroupBaseStyle + " justify-between cursor-pointer",
|
|
9
|
-
triggerButtonStyle: "w-full cursor-pointer bg-
|
|
9
|
+
triggerButtonStyle: "w-full cursor-pointer bg-[var(--color-surface)]",
|
|
10
10
|
placeholderTextStyle: "text-[var(--color-text-placeholder)]",
|
|
11
11
|
valueTextStyle: "text-[var(--color-text-primary)]",
|
|
12
12
|
triggerSizes: {
|
|
@@ -19,26 +19,19 @@ const dateRangePickerTheme = {
|
|
|
19
19
|
triggerFocusStates: formsBaseTheme.focusStates,
|
|
20
20
|
iconEndStyle: formsBaseTheme.iconStyles.right,
|
|
21
21
|
iconEndPadding: formsBaseTheme.iconStyles.padding.right,
|
|
22
|
-
bodyStyle: "flex flex-col sm:flex-row w-fit max-w-[calc(100vw-2rem)] sm:max-w-none bg-
|
|
23
|
-
|
|
24
|
-
presetsContainerStyle: "hidden sm:flex flex-col gap-1 p-3 sm:border-r border-[var(--color-border)] bg-[var(--color-neutral-50)] sm:min-w-[180px] " +
|
|
25
|
-
"dark:bg-[var(--color-neutral-800)] ",
|
|
22
|
+
bodyStyle: "flex flex-col sm:flex-row w-fit max-w-[calc(100vw-2rem)] sm:max-w-none bg-[var(--color-surface-overlay)] border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-lg p-0 overflow-hidden z-[1000] fixed",
|
|
23
|
+
presetsContainerStyle: "hidden sm:flex flex-col gap-1 p-3 sm:border-r border-[var(--color-border)] bg-[var(--color-surface-sunken)] sm:min-w-[180px]",
|
|
26
24
|
presetButtonStyle: "w-full text-left px-3 py-2 text-sm font-medium rounded-[calc(var(--radius-base)*0.75)] transition-colors cursor-pointer " +
|
|
27
25
|
"flex items-center justify-between gap-2 " +
|
|
28
|
-
"text-[var(--color-text-secondary)] hover:bg-
|
|
29
|
-
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 " +
|
|
30
|
-
"data-[active=true]:bg-
|
|
31
|
-
"dark:hover:bg-[var(--color-neutral-700)] " +
|
|
32
|
-
"dark:data-[active=true]:bg-[var(--color-neutral-700)] dark:data-[active=true]:text-[var(--color-neutral-100)] " +
|
|
33
|
-
"dark:focus-visible:ring-offset-[var(--color-neutral-800)]",
|
|
26
|
+
"text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] " +
|
|
27
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-sunken)] " +
|
|
28
|
+
"data-[active=true]:bg-[var(--color-surface)] data-[active=true]:text-[var(--color-text-primary)] data-[active=true]:font-medium",
|
|
34
29
|
presetCheckmarkStyle: "size-4 text-[var(--color-primary)] shrink-0",
|
|
35
30
|
mobilePresetsContainerStyle: "sm:hidden px-3 pt-3 pb-1",
|
|
36
31
|
mobilePresetButtonStyle: "whitespace-nowrap px-3 py-1.5 text-xs font-medium rounded-full transition-colors cursor-pointer shrink-0 " +
|
|
37
32
|
"bg-[var(--color-background-secondary)] text-[var(--color-text-secondary)] " +
|
|
38
33
|
"hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] " +
|
|
39
34
|
"data-[active=true]:bg-[var(--color-primary-50)] data-[active=true]:text-[var(--color-primary)] " +
|
|
40
|
-
"dark:bg-[var(--color-neutral-800)] dark:text-[var(--color-neutral-300)] " +
|
|
41
|
-
"dark:hover:bg-[var(--color-neutral-700)] " +
|
|
42
35
|
"dark:data-[active=true]:bg-[var(--color-primary-950)] dark:data-[active=true]:text-[var(--color-primary-300)]",
|
|
43
36
|
mobileRangeDisplayStyle: "sm:hidden text-xs text-center py-2 text-[var(--color-text-secondary)] dark:text-[var(--color-neutral-400)]",
|
|
44
37
|
helperTextStyle: formsBaseTheme.helperText,
|
|
@@ -24,7 +24,7 @@ const DateRangePickerTrigger = ({ id, disabled, isOpen, state = "default", size
|
|
|
24
24
|
const triggerButtonStyle = theme.triggerButtonStyle || "";
|
|
25
25
|
const activeFocusState = isOpen
|
|
26
26
|
? focusStateStyle ||
|
|
27
|
-
"shadow-[
|
|
27
|
+
"shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10"
|
|
28
28
|
: "";
|
|
29
29
|
return (jsx("div", { className: "relative w-full", children: jsxs("button", { ref: triggerRef, id: id, type: "button", className: cn(baseStyle, sizeStyle, stateStyle, hoverStateStyle, activeFocusState, iconEndPadding, triggerButtonStyle, "text-left", className), onClick: onClick, onFocus: onFocus, onBlur: onBlur, disabled: disabled, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-label": "Open date range picker", "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, children: [children, jsx("span", { className: iconEndStyle, children: jsx(CalendarIcon, { className: "h-4 w-4" }) })] }) }));
|
|
30
30
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Spring used for thumbnail enter and for the layout reflow when items are removed. */
|
|
2
|
+
export declare const PREVIEW_LAYOUT_SPRING: {
|
|
3
|
+
type: "spring";
|
|
4
|
+
stiffness: number;
|
|
5
|
+
damping: number;
|
|
6
|
+
mass: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Enter / exit variants for a preview thumbnail.
|
|
10
|
+
* Exit shrinks + fades so the surrounding items can flow into the gap (driven
|
|
11
|
+
* by `layout` on each item). Pair with `AnimatePresence`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PREVIEW_ITEM: {
|
|
14
|
+
initial: {
|
|
15
|
+
opacity: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
};
|
|
18
|
+
animate: {
|
|
19
|
+
opacity: number;
|
|
20
|
+
scale: number;
|
|
21
|
+
};
|
|
22
|
+
exit: {
|
|
23
|
+
opacity: number;
|
|
24
|
+
scale: number;
|
|
25
|
+
transition: {
|
|
26
|
+
duration: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Spring used for thumbnail enter and for the layout reflow when items are removed. */
|
|
2
|
+
const PREVIEW_LAYOUT_SPRING = {
|
|
3
|
+
type: "spring",
|
|
4
|
+
stiffness: 500,
|
|
5
|
+
damping: 38,
|
|
6
|
+
mass: 0.8,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Enter / exit variants for a preview thumbnail.
|
|
10
|
+
* Exit shrinks + fades so the surrounding items can flow into the gap (driven
|
|
11
|
+
* by `layout` on each item). Pair with `AnimatePresence`.
|
|
12
|
+
*/
|
|
13
|
+
const PREVIEW_ITEM = {
|
|
14
|
+
initial: { opacity: 0, scale: 0.8 },
|
|
15
|
+
animate: { opacity: 1, scale: 1 },
|
|
16
|
+
exit: { opacity: 0, scale: 0.8, transition: { duration: 0.15 } },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { PREVIEW_ITEM, PREVIEW_LAYOUT_SPRING };
|
|
@@ -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';
|
|
@@ -82,6 +85,8 @@ import '../../core/Sidebar/SidebarToggle.js';
|
|
|
82
85
|
import '../../core/Sidebar/SidebarMobileTrigger.js';
|
|
83
86
|
import '../../core/Sidebar/SidebarUserProfile.js';
|
|
84
87
|
import '../../core/Sidebar/SidebarLogo.js';
|
|
88
|
+
import '../../core/Sidebar/SidebarSearch.js';
|
|
89
|
+
import '../../core/Sidebar/SidebarCard.js';
|
|
85
90
|
import '../../core/Sidebar/SidebarContext.js';
|
|
86
91
|
import '../../core/OfflineIndicator/OfflineIndicator.js';
|
|
87
92
|
import '../../core/ContextMenu/ContextMenu.js';
|
|
@@ -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 {};
|