@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.
Files changed (131) hide show
  1. package/dist/components/ai/PromptInput/PromptInput.js +3 -0
  2. package/dist/components/ai/PromptInput/VoiceRecorder.js +3 -0
  3. package/dist/components/core/Button/Button.theme.js +1 -1
  4. package/dist/components/core/Calendar/Calendar.theme.js +1 -1
  5. package/dist/components/core/CommandPalette/CommandItem.js +4 -5
  6. package/dist/components/core/CommandPalette/CommandPalette.js +1 -1
  7. package/dist/components/core/CommandPalette/CommandPalette.theme.js +11 -13
  8. package/dist/components/core/CommandPalette/CommandPalette.types.d.ts +0 -2
  9. package/dist/components/core/ContextMenu/ContextMenu.theme.js +13 -14
  10. package/dist/components/core/DotSeparator/DotSeparator.d.ts +3 -0
  11. package/dist/components/core/DotSeparator/DotSeparator.js +19 -0
  12. package/dist/components/core/DotSeparator/DotSeparator.theme.d.ts +2 -0
  13. package/dist/components/core/DotSeparator/DotSeparator.theme.js +10 -0
  14. package/dist/components/core/DotSeparator/DotSeparator.types.d.ts +25 -0
  15. package/dist/components/core/DotSeparator/index.d.ts +3 -0
  16. package/dist/components/core/Dropdown/Dropdown.theme.js +2 -3
  17. package/dist/components/core/Dropdown/DropdownItem.js +1 -1
  18. package/dist/components/core/Dropdown/DropdownMenu.js +1 -1
  19. package/dist/components/core/MenuItem/MenuItem.js +5 -2
  20. package/dist/components/core/MenuItem/MenuItem.theme.d.ts +6 -0
  21. package/dist/components/core/MenuItem/MenuItem.theme.js +19 -16
  22. package/dist/components/core/MenuItem/MenuItem.types.d.ts +2 -0
  23. package/dist/components/core/NavItem/NavItem.js +20 -4
  24. package/dist/components/core/NavItem/NavItem.theme.js +13 -2
  25. package/dist/components/core/Popover/Popover.types.d.ts +6 -2
  26. package/dist/components/core/Popover/PopoverBody.js +10 -3
  27. package/dist/components/core/Segmented/Segmented.theme.js +1 -1
  28. package/dist/components/core/Sidebar/Sidebar.d.ts +2 -0
  29. package/dist/components/core/Sidebar/Sidebar.js +15 -7
  30. package/dist/components/core/Sidebar/Sidebar.types.d.ts +92 -4
  31. package/dist/components/core/Sidebar/SidebarCard.d.ts +25 -0
  32. package/dist/components/core/Sidebar/SidebarCard.js +68 -0
  33. package/dist/components/core/Sidebar/SidebarCard.theme.d.ts +8 -0
  34. package/dist/components/core/Sidebar/SidebarCard.theme.js +18 -0
  35. package/dist/components/core/Sidebar/SidebarContent.js +5 -1
  36. package/dist/components/core/Sidebar/SidebarContext.js +1 -1
  37. package/dist/components/core/Sidebar/SidebarHeader.js +10 -2
  38. package/dist/components/core/Sidebar/SidebarLogo.js +38 -3
  39. package/dist/components/core/Sidebar/SidebarSearch.d.ts +22 -0
  40. package/dist/components/core/Sidebar/SidebarSearch.js +135 -0
  41. package/dist/components/core/Sidebar/SidebarToggle.js +1 -1
  42. package/dist/components/core/Sidebar/SidebarUserProfile.js +2 -2
  43. package/dist/components/core/Sidebar/index.d.ts +3 -1
  44. package/dist/components/core/Tag/Tag.js +29 -5
  45. package/dist/components/core/Tag/Tag.theme.js +13 -8
  46. package/dist/components/core/Tag/Tag.types.d.ts +7 -0
  47. package/dist/components/core/Tooltip/Tooltip.js +1 -1
  48. package/dist/components/core/Tooltip/Tooltip.theme.js +1 -2
  49. package/dist/components/core/index.d.ts +1 -0
  50. package/dist/components/core/index.js +4 -0
  51. package/dist/components/data-display/GanttChart/GanttChart.d.ts +1 -1
  52. package/dist/components/data-display/GanttChart/GanttChart.js +31 -4
  53. package/dist/components/data-display/GanttChart/GanttChart.theme.js +19 -17
  54. package/dist/components/data-display/GanttChart/GanttChart.types.d.ts +19 -0
  55. package/dist/components/data-display/GanttChart/GanttChart.utils.js +1 -1
  56. package/dist/components/data-display/GanttChart/GanttChartPanel.d.ts +1 -1
  57. package/dist/components/data-display/GanttChart/GanttChartPanel.js +3 -3
  58. package/dist/components/data-display/GanttChart/GanttDependencyArrows.js +7 -4
  59. package/dist/components/data-display/GanttChart/GanttMilestone.js +5 -5
  60. package/dist/components/data-display/GanttChart/GanttTablePanel.js +1 -1
  61. package/dist/components/data-display/GanttChart/GanttTableTaskRow.js +2 -2
  62. package/dist/components/data-display/GanttChart/GanttTaskBar.js +37 -16
  63. package/dist/components/data-display/GanttChart/GanttTimelineGrid.js +2 -1
  64. package/dist/components/data-display/GanttChart/GanttTimelineHeader.js +35 -11
  65. package/dist/components/data-display/GanttChart/GanttToolbar.js +2 -2
  66. package/dist/components/data-display/GanttChart/hooks/useGanttDrag.js +3 -2
  67. package/dist/components/data-display/GanttChart/index.d.ts +1 -1
  68. package/dist/components/data-display/Table/Table.theme.js +3 -5
  69. package/dist/components/effects/Aurora/Aurora.d.ts +6 -17
  70. package/dist/components/effects/Aurora/Aurora.js +297 -131
  71. package/dist/components/effects/Aurora/Aurora.types.d.ts +6 -0
  72. package/dist/components/forms/Combobox/Combobox.js +1 -1
  73. package/dist/components/forms/Combobox/Combobox.theme.js +2 -4
  74. package/dist/components/forms/CronInput/CronInput.js +1 -1
  75. package/dist/components/forms/CronInput/CronInput.theme.js +14 -23
  76. package/dist/components/forms/DatePicker/DatePicker.theme.js +11 -18
  77. package/dist/components/forms/DatePicker/DatePickerTrigger.js +1 -1
  78. package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +6 -13
  79. package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +1 -1
  80. package/dist/components/forms/FileUpload/FileUpload.animations.d.ts +29 -0
  81. package/dist/components/forms/FileUpload/FileUpload.animations.js +19 -0
  82. package/dist/components/forms/FileUpload/FileUpload.js +87 -48
  83. package/dist/components/forms/FileUpload/FileUpload.theme.d.ts +2 -10
  84. package/dist/components/forms/FileUpload/FileUpload.theme.js +16 -30
  85. package/dist/components/forms/FileUpload/FileUpload.types.d.ts +26 -10
  86. package/dist/components/forms/Input/Input.theme.js +13 -10
  87. package/dist/components/forms/InputCounter/InputCounter.theme.js +1 -1
  88. package/dist/components/forms/Mention/Mention.js +152 -57
  89. package/dist/components/forms/Mention/Mention.theme.js +16 -23
  90. package/dist/components/forms/Mention/Mention.types.d.ts +12 -12
  91. package/dist/components/forms/Mention/Mention.utils.js +32 -4
  92. package/dist/components/forms/RichTextEditor/RichTextEditor.js +3 -3
  93. package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +5 -5
  94. package/dist/components/forms/Select/Select.js +2 -2
  95. package/dist/components/forms/Select/Select.theme.js +3 -3
  96. package/dist/components/forms/TimePicker/TimePickerContent.js +3 -0
  97. package/dist/components/forms/TimePicker/TimePickerTrigger.js +1 -1
  98. package/dist/components/forms/forms.theme.js +21 -16
  99. package/dist/components/forms/index.js +3 -0
  100. package/dist/components/layout/AppShell/AppShell.d.ts +10 -0
  101. package/dist/components/layout/AppShell/AppShell.js +84 -0
  102. package/dist/components/layout/AppShell/AppShell.theme.d.ts +3 -0
  103. package/dist/components/layout/AppShell/AppShell.theme.js +42 -0
  104. package/dist/components/layout/AppShell/AppShell.types.d.ts +127 -0
  105. package/dist/components/layout/AppShell/AppShellContext.d.ts +14 -0
  106. package/dist/components/layout/AppShell/AppShellContext.js +19 -0
  107. package/dist/components/layout/AppShell/AppShellFooter.d.ts +3 -0
  108. package/dist/components/layout/AppShell/AppShellFooter.js +12 -0
  109. package/dist/components/layout/AppShell/AppShellHeader.d.ts +3 -0
  110. package/dist/components/layout/AppShell/AppShellHeader.js +12 -0
  111. package/dist/components/layout/AppShell/AppShellMain.d.ts +3 -0
  112. package/dist/components/layout/AppShell/AppShellMain.js +19 -0
  113. package/dist/components/layout/AppShell/AppShellRightPanel.d.ts +3 -0
  114. package/dist/components/layout/AppShell/AppShellRightPanel.js +16 -0
  115. package/dist/components/layout/AppShell/AppShellSidebar.d.ts +3 -0
  116. package/dist/components/layout/AppShell/AppShellSidebar.js +16 -0
  117. package/dist/components/layout/AppShell/AppShellTopbar.d.ts +3 -0
  118. package/dist/components/layout/AppShell/AppShellTopbar.js +12 -0
  119. package/dist/components/layout/AppShell/index.d.ts +3 -0
  120. package/dist/components/layout/FormLayout/FormLayout.js +3 -0
  121. package/dist/components/layout/index.d.ts +1 -0
  122. package/dist/components/layout/index.js +2 -0
  123. package/dist/hooks/useSelectPortal.d.ts +10 -2
  124. package/dist/hooks/useSelectPortal.js +45 -24
  125. package/dist/index.js +6 -0
  126. package/dist/registry.json +10021 -0
  127. package/dist/styles.css +1 -1
  128. package/dist/utils/dateUtils.js +11 -1
  129. package/package.json +4 -2
  130. package/src/global.scss +29 -8
  131. 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-background-secondary)] text-[var(--color-text-primary)]", "dark:bg-[var(--color-neutral-800)] dark:text-[var(--color-neutral-200)]"), 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
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)] dark:text-[var(--color-neutral-200)]",
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)] dark:text-[var(--color-neutral-400)]",
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-white text-[var(--color-text-primary)] cursor-pointer transition-all " +
21
- "focus:shadow-[inset_0_0_0_2px_var(--color-primary-600)] " +
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:bg-[var(--color-neutral-900)] dark:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] dark:text-[var(--color-neutral-200)] " +
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-white text-[var(--color-text-primary)] font-mono transition-all " +
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-[inset_0_0_0_2px_var(--color-primary-600)] " +
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:bg-[var(--color-neutral-900)] dark:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] dark:text-[var(--color-neutral-200)] " +
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-white hover:bg-[var(--color-background-secondary)] " +
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-white hover:bg-[var(--color-background-secondary)] " +
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 - bg-white for light mode; dark mode uses neutral-900
14
- triggerButtonStyle: "w-full cursor-pointer bg-white dark:bg-[var(--color-neutral-900)]",
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 - with dark mode support
30
- bodyStyle: "min-w-fit max-w-none bg-white border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-real-xl p-0 overflow-hidden z-[1000] fixed " +
31
- "dark:bg-[var(--color-neutral-900)] dark:border-[var(--color-neutral-700)]",
32
- bodyWithPresetsStyle: "flex flex-col sm:flex-row w-fit max-w-[calc(100vw-2rem)] sm:max-w-none bg-white border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-lg p-0 overflow-hidden z-[1000] fixed " +
33
- "dark:bg-[var(--color-neutral-900)] dark:border-[var(--color-neutral-700)]",
34
- // Presets sidebar - with dark mode support
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-white hover:text-[var(--color-text-primary)] " +
40
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 " +
41
- "data-[active=true]:bg-white data-[active=true]:text-[var(--color-text-primary)] data-[active=true]:font-medium " +
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-[inset_0_0_0_2px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10"
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-white dark:bg-[var(--color-neutral-900)]",
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-white border border-[var(--color-border)] rounded-[var(--popover-radius)] shadow-lg p-0 overflow-hidden z-[1000] fixed " +
23
- "dark:bg-[var(--color-neutral-900)] dark:border-[var(--color-neutral-700)]",
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-white hover:text-[var(--color-text-primary)] " +
29
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 " +
30
- "data-[active=true]:bg-white data-[active=true]:text-[var(--color-text-primary)] data-[active=true]:font-medium " +
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-[inset_0_0_0_2px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10"
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, useCallback } from 'react';
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
- const [selectedFiles, setSelectedFiles] = useState([]);
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
- // Create preview URL for images
154
- const createPreview = useCallback((file) => {
155
- if (file.type.startsWith("image/")) {
156
- const url = URL.createObjectURL(file);
157
- setPreviewUrls((prev) => new Map(prev).set(file, url));
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
- // Update selected files
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
- setSelectedFiles(newFiles);
217
- // Create previews
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
- createPreview,
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
- setSelectedFiles((prev) => prev.filter((f) => f !== file));
273
- revokePreview(file);
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
- }, [onRemove, revokePreview]);
276
- // Format file size
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: theme.contentStyle, children: [jsx("div", { className: "size-11 border border-border bg-white rounded-full flex items-center justify-center mb-4", children: jsx(CloudArrowUpIcon, { className: `size-6 text-[var(--color-text-secondary)]` }) }), jsxs("p", { className: "text-base font-medium text-[var(--color-text-primary)]", children: [jsx("span", { className: "text-[var(--color-primary)] font-semibold", children: buttonText }), " ", helperText] }), description && (jsx("p", { className: "text-sm text-[var(--color-text-muted)] mt-1", children: description }))] }) }), errorMessage && (jsx("div", { className: "text-sm mt-1 text-[var(--color-danger)]", children: errorMessage })), showPreview && selectedFiles.length > 0 && (jsx("div", { className: theme.previewContainerStyle, children: selectedFiles.map((file, index) => {
291
- const previewUrl = previewUrls.get(file);
292
- const isImage = file.type.startsWith("image/");
293
- return (jsxs("div", { className: theme.previewItemStyle, children: [isImage && previewUrl ? (jsx("img", { src: previewUrl, alt: file.name, className: theme.previewImageStyle })) : (jsxs("div", { className: theme.previewFileStyle, children: [jsx(DocumentIcon, { className: "w-8 h-8 mx-auto mb-2 text-[var(--color-text-muted)]" }), jsx("span", { className: "text-sm", children: file.name }), jsx("span", { className: "text-sm text-[var(--color-text-placeholder)] mt-1", children: formatFileSize(file.size) })] })), jsx(Button, { variant: "filled", color: "danger", size: "sm", iconOnly: true, onClick: (e) => {
294
- e.stopPropagation();
295
- handleRemoveFile(file);
296
- }, "aria-label": `Remove ${file.name}`, className: cn(theme.removeButtonStyle, "rounded-full !p-0 size-6"), children: jsx(XMarkIcon, { className: "size-4", strokeWidth: 2 }) })] }, `${file.name}-${index}`));
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
- * Internal theme structure for the FileUpload component
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 {};