@nubitio/ui 0.5.15 → 0.5.16

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/index.cjs CHANGED
@@ -25,6 +25,7 @@ let react = require("react");
25
25
  react = __toESM(react, 1);
26
26
  let react_jsx_runtime = require("react/jsx-runtime");
27
27
  let react_dom = require("react-dom");
28
+ let react_dropzone = require("react-dropzone");
28
29
  //#region packages/ui/Avatar.tsx
29
30
  /**
30
31
  * 8 semantic hues aligned with the Fluent / @nubitio design palette.
@@ -83,13 +84,13 @@ const Avatar = ({ owner = "", size, variant = "md", shape = "circle", alt, class
83
84
  };
84
85
  //#endregion
85
86
  //#region packages/ui/Button.tsx
86
- const cx$4 = (...values) => values.filter(Boolean).join(" ");
87
+ const cx$5 = (...values) => values.filter(Boolean).join(" ");
87
88
  const Button = ({ variant = "secondary", size = "md", fullWidth = false, icon, loading = false, className, children, disabled, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
88
89
  ...props,
89
90
  type: props.type ?? "button",
90
91
  disabled: disabled || loading,
91
92
  "aria-busy": loading || void 0,
92
- className: cx$4("nb-button", `nb-button--${variant}`, size === "sm" && "nb-button--sm", fullWidth && "nb-button--full", className),
93
+ className: cx$5("nb-button", `nb-button--${variant}`, size === "sm" && "nb-button--sm", fullWidth && "nb-button--full", className),
93
94
  children: [loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
94
95
  className: "nb-button-spinner",
95
96
  "aria-hidden": "true"
@@ -103,7 +104,7 @@ const IconButton = ({ icon, label, variant = "default", className, ...props }) =
103
104
  type: props.type ?? "button",
104
105
  "aria-label": props["aria-label"] ?? label,
105
106
  title: props.title ?? label,
106
- className: cx$4("nb-icon-button", variant === "danger" && "nb-icon-button--danger", className),
107
+ className: cx$5("nb-icon-button", variant === "danger" && "nb-icon-button--danger", className),
107
108
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
108
109
  className: icon,
109
110
  "aria-hidden": "true"
@@ -352,10 +353,10 @@ const Card = ({ title, description, children }) => {
352
353
  };
353
354
  //#endregion
354
355
  //#region packages/ui/FormControls.tsx
355
- const cx$3 = (...values) => values.filter(Boolean).join(" ");
356
+ const cx$4 = (...values) => values.filter(Boolean).join(" ");
356
357
  const FormField = ({ label, error, helpText, children, className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
357
358
  ...props,
358
- className: cx$3("nb-form-field", !!error && "nb-form-field--error", className),
359
+ className: cx$4("nb-form-field", !!error && "nb-form-field--error", className),
359
360
  children: [
360
361
  label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
361
362
  className: "nb-form-field__label",
@@ -376,14 +377,14 @@ const TextField = (0, react.forwardRef)(function TextField({ className, invalid,
376
377
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
377
378
  ...props,
378
379
  ref,
379
- className: cx$3("nb-input", invalid && "nb-input--invalid", className)
380
+ className: cx$4("nb-input", invalid && "nb-input--invalid", className)
380
381
  });
381
382
  });
382
383
  const SelectField = (0, react.forwardRef)(function SelectField({ className, invalid, children, ...props }, ref) {
383
384
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
384
385
  ...props,
385
386
  ref,
386
- className: cx$3("nb-input", "nb-select", invalid && "nb-input--invalid", className),
387
+ className: cx$4("nb-input", "nb-select", invalid && "nb-input--invalid", className),
387
388
  children
388
389
  });
389
390
  });
@@ -391,10 +392,151 @@ const TextAreaField = (0, react.forwardRef)(function TextAreaField({ className,
391
392
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
392
393
  ...props,
393
394
  ref,
394
- className: cx$3("nb-input", "nb-textarea", invalid && "nb-input--invalid", className)
395
+ className: cx$4("nb-input", "nb-textarea", invalid && "nb-input--invalid", className)
395
396
  });
396
397
  });
397
398
  //#endregion
399
+ //#region packages/ui/FileDropzone.tsx
400
+ function cx$3(...values) {
401
+ return values.filter(Boolean).join(" ");
402
+ }
403
+ function buildDropzoneAccept(accept) {
404
+ if (!accept || accept === "*/*" || accept === "*") return void 0;
405
+ if (accept === "image/*") return {
406
+ "image/png": [".png"],
407
+ "image/jpeg": [".jpg", ".jpeg"],
408
+ "image/webp": [".webp"],
409
+ "image/gif": [".gif"]
410
+ };
411
+ if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
412
+ const trimmed = token.trim();
413
+ if (!trimmed) return acc;
414
+ if (trimmed.startsWith(".")) {
415
+ acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
416
+ return acc;
417
+ }
418
+ acc[trimmed] = [];
419
+ return acc;
420
+ }, {});
421
+ if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
422
+ return { [accept]: [] };
423
+ }
424
+ function FileDropzone({ accept, value = null, image = false, disabled = false, readOnly = false, invalid = false, uploading = false, error = null, labels, inputId, inputLabel, className, onFileSelect, onClear }) {
425
+ const previewUrl = value?.previewUrl ?? null;
426
+ const fileName = value?.fileName ?? null;
427
+ const fileUrl = value?.fileUrl ?? null;
428
+ const hasContent = !!(previewUrl || fileName);
429
+ const isInteractive = !disabled && !readOnly;
430
+ const { getRootProps, getInputProps, isDragActive, open } = (0, react_dropzone.useDropzone)({
431
+ accept: buildDropzoneAccept(accept),
432
+ disabled: disabled || readOnly || uploading,
433
+ multiple: false,
434
+ noClick: hasContent,
435
+ noKeyboard: hasContent,
436
+ onDrop: (acceptedFiles) => {
437
+ const file = acceptedFiles[0];
438
+ if (file) onFileSelect(file);
439
+ }
440
+ });
441
+ const placeholderIcon = image ? "ph-image" : "ph-file-arrow-up";
442
+ const placeholderTitle = isDragActive ? labels?.dropPrompt ?? "Drop file here" : image ? labels?.imagePrompt ?? "Drop image here or click to browse" : labels?.prompt ?? "Drop file here or click to browse";
443
+ const placeholderHint = image ? labels?.imageHint ?? "PNG, JPG, WebP or GIF" : labels?.hint ?? "Select one file";
444
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
445
+ className: cx$3("nb-file-dropzone", image && "nb-file-dropzone--image", invalid && "nb-file-dropzone--invalid", className),
446
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
447
+ ...getRootProps({ className: cx$3("nb-file-dropzone__zone", isDragActive && "nb-file-dropzone__zone--active", hasContent && "nb-file-dropzone__zone--filled", uploading && "nb-file-dropzone__zone--uploading", !isInteractive && "nb-file-dropzone__zone--disabled") }),
448
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps({
449
+ id: inputId,
450
+ "aria-label": inputLabel
451
+ }) }), hasContent ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
452
+ previewUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
453
+ className: "nb-file-dropzone__preview",
454
+ src: previewUrl,
455
+ alt: inputLabel ?? fileName ?? ""
456
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
457
+ className: "nb-file-dropzone__file",
458
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
459
+ className: "nb-file-dropzone__file-icon",
460
+ "aria-hidden": "true",
461
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "ph ph-file" })
462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
463
+ className: "nb-file-dropzone__file-meta",
464
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
465
+ className: "nb-file-dropzone__file-name",
466
+ children: fileName
467
+ }), fileUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
468
+ className: "nb-file-dropzone__file-link",
469
+ href: fileUrl,
470
+ target: "_blank",
471
+ rel: "noreferrer",
472
+ onClick: (event) => event.stopPropagation(),
473
+ children: labels?.open ?? "Open"
474
+ })]
475
+ })]
476
+ }),
477
+ uploading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
478
+ className: "nb-file-dropzone__overlay",
479
+ "aria-live": "polite",
480
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
481
+ className: "nb-file-dropzone__spinner",
482
+ "aria-hidden": "true"
483
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labels?.uploading ?? "Uploading..." })]
484
+ }),
485
+ isInteractive && !uploading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
486
+ className: "nb-file-dropzone__actions",
487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
488
+ type: "button",
489
+ className: "nb-file-dropzone__action",
490
+ onClick: (event) => {
491
+ event.stopPropagation();
492
+ open();
493
+ },
494
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
495
+ className: "ph ph-arrows-clockwise",
496
+ "aria-hidden": "true"
497
+ }), labels?.replace ?? "Replace"]
498
+ }), onClear && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
499
+ type: "button",
500
+ className: "nb-file-dropzone__action nb-file-dropzone__action--danger",
501
+ onClick: (event) => {
502
+ event.stopPropagation();
503
+ onClear();
504
+ },
505
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
506
+ className: "ph ph-trash",
507
+ "aria-hidden": "true"
508
+ }), labels?.remove ?? "Remove"]
509
+ })]
510
+ })
511
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
512
+ className: "nb-file-dropzone__placeholder",
513
+ children: [
514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
515
+ className: "nb-file-dropzone__icon",
516
+ "aria-hidden": "true",
517
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: `ph ${placeholderIcon}` })
518
+ }),
519
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
520
+ className: "nb-file-dropzone__title",
521
+ children: placeholderTitle
522
+ }),
523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
524
+ className: "nb-file-dropzone__hint",
525
+ children: placeholderHint
526
+ })
527
+ ]
528
+ })]
529
+ }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
530
+ className: "nb-file-dropzone__error",
531
+ role: "alert",
532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
533
+ className: "ph ph-warning-circle",
534
+ "aria-hidden": "true"
535
+ }), error]
536
+ })]
537
+ });
538
+ }
539
+ //#endregion
398
540
  //#region packages/ui/AppDropdown.tsx
399
541
  function cx$2(...values) {
400
542
  return values.filter(Boolean).join(" ");
@@ -2358,11 +2500,16 @@ const TimelineVariantContext = (0, react.createContext)("stepper");
2358
2500
  function useTimelineVariant() {
2359
2501
  return (0, react.useContext)(TimelineVariantContext);
2360
2502
  }
2361
- function TimelineMarker({ status, variant }) {
2503
+ function TimelineMarker({ status, variant, marker }) {
2362
2504
  if (variant === "log") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2363
2505
  className: "nb-timeline__marker nb-timeline__marker--dot",
2364
2506
  "aria-hidden": "true"
2365
2507
  });
2508
+ if (marker != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2509
+ className: `nb-timeline__marker nb-timeline__marker--${status} nb-timeline__marker--custom`,
2510
+ "aria-hidden": "true",
2511
+ children: marker
2512
+ });
2366
2513
  if (status === "complete") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2367
2514
  className: "nb-timeline__marker nb-timeline__marker--complete",
2368
2515
  "aria-hidden": "true",
@@ -2382,7 +2529,7 @@ function TimelineMarker({ status, variant }) {
2382
2529
  "aria-hidden": "true"
2383
2530
  });
2384
2531
  }
2385
- function TimelineItem({ status, title, timestamp, dateTime, tone = "default", children, className }) {
2532
+ function TimelineItem({ status, title, marker, timestamp, dateTime, tone = "default", children, className }) {
2386
2533
  const variant = useTimelineVariant();
2387
2534
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2388
2535
  className: [
@@ -2395,7 +2542,8 @@ function TimelineItem({ status, title, timestamp, dateTime, tone = "default", ch
2395
2542
  className: "nb-timeline__marker-col",
2396
2543
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineMarker, {
2397
2544
  status,
2398
- variant
2545
+ variant,
2546
+ marker
2399
2547
  })
2400
2548
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2401
2549
  className: "nb-timeline__content",
@@ -2463,6 +2611,7 @@ exports.Drawer = Drawer;
2463
2611
  exports.EN_UI_STRINGS = EN_UI_STRINGS;
2464
2612
  exports.ES_UI_STRINGS = ES_UI_STRINGS;
2465
2613
  exports.EmptyState = EmptyState;
2614
+ exports.FileDropzone = FileDropzone;
2466
2615
  exports.FormField = FormField;
2467
2616
  exports.IconButton = IconButton;
2468
2617
  exports.Popover = Popover;
package/dist/index.d.cts CHANGED
@@ -204,6 +204,56 @@ interface TextAreaFieldProps extends TextareaHTMLAttributes<HTMLTextAreaElement>
204
204
  }
205
205
  declare const TextAreaField: import("react").ForwardRefExoticComponent<TextAreaFieldProps & import("react").RefAttributes<HTMLTextAreaElement>>;
206
206
  //#endregion
207
+ //#region packages/ui/FileDropzone.d.ts
208
+ interface FileDropzoneValue {
209
+ fileName?: string | null;
210
+ fileUrl?: string | null;
211
+ previewUrl?: string | null;
212
+ }
213
+ interface FileDropzoneLabels {
214
+ prompt?: string;
215
+ imagePrompt?: string;
216
+ dropPrompt?: string;
217
+ hint?: string;
218
+ imageHint?: string;
219
+ uploading?: string;
220
+ replace?: string;
221
+ remove?: string;
222
+ open?: string;
223
+ }
224
+ interface FileDropzoneProps {
225
+ accept?: string | null;
226
+ value?: FileDropzoneValue | null;
227
+ image?: boolean;
228
+ disabled?: boolean;
229
+ readOnly?: boolean;
230
+ invalid?: boolean;
231
+ uploading?: boolean;
232
+ error?: string | null;
233
+ labels?: FileDropzoneLabels;
234
+ inputId?: string;
235
+ inputLabel?: string;
236
+ className?: string;
237
+ onFileSelect: (file: File) => void;
238
+ onClear?: () => void;
239
+ }
240
+ declare function FileDropzone({
241
+ accept,
242
+ value,
243
+ image,
244
+ disabled,
245
+ readOnly,
246
+ invalid,
247
+ uploading,
248
+ error,
249
+ labels,
250
+ inputId,
251
+ inputLabel,
252
+ className,
253
+ onFileSelect,
254
+ onClear
255
+ }: FileDropzoneProps): import("react").JSX.Element;
256
+ //#endregion
207
257
  //#region packages/ui/AppDropdown.d.ts
208
258
  interface AppDropdownOption {
209
259
  value: string;
@@ -739,6 +789,8 @@ interface TimelineProps {
739
789
  interface TimelineItemProps {
740
790
  status: TimelineItemStatus;
741
791
  title: ReactNode;
792
+ /** Custom stepper marker content, for example a step number. Ignored by the `log` variant. */
793
+ marker?: ReactNode;
742
794
  timestamp?: ReactNode;
743
795
  dateTime?: string;
744
796
  /** Marker accent for the `log` variant. */
@@ -749,6 +801,7 @@ interface TimelineItemProps {
749
801
  declare function TimelineItem({
750
802
  status,
751
803
  title,
804
+ marker,
752
805
  timestamp,
753
806
  dateTime,
754
807
  tone,
@@ -826,4 +879,4 @@ interface UiStringsProviderProps {
826
879
  declare const UiStringsProvider: React$1.FC<UiStringsProviderProps>;
827
880
  declare const useUiStrings: () => UiStrings;
828
881
  //#endregion
829
- export { ACCENT_PRESETS, type AccentPreset, AppDialog, type AppDialogProps, AppDropdown, type AppDropdownOption, type AppDropdownProps, type AppDropdownVariant, AppToolbar, type AppToolbarProps, Avatar, type AvatarProps, type AvatarShape, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Chip, type ChipProps, CollapsibleSection, type CollapsibleSectionProps, ConfirmDialog, type ConfirmDialogProps, ContextMenu, type ContextMenuItem, type ContextMenuProps, DatePicker, type DatePickerProps, DateRangePicker, type DateRangePickerProps, type Density, DensityContext, type DensityContextValue, DensityProvider, Drawer, type DrawerProps, type DrawerSide, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, IconButton, type IconButtonProps, Popover, type PopoverAlign, type PopoverProps, SelectField, type SelectFieldProps, SettingsPanel, type SettingsPanelProps, Skeleton, type SkeletonProps, StatCard, type StatCardProps, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type Theme, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThemeSwitcher, type ThemeSwitcherProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineItemStatus, type TimelineItemTone, type TimelineOrientation, type TimelineProps, type TimelineVariant, Toggle, type ToggleProps, type UiStrings, UiStringsProvider, type UiStringsProviderProps, type UseFloatingPanelOptions, type UseFloatingPanelResult, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
882
+ export { ACCENT_PRESETS, type AccentPreset, AppDialog, type AppDialogProps, AppDropdown, type AppDropdownOption, type AppDropdownProps, type AppDropdownVariant, AppToolbar, type AppToolbarProps, Avatar, type AvatarProps, type AvatarShape, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Chip, type ChipProps, CollapsibleSection, type CollapsibleSectionProps, ConfirmDialog, type ConfirmDialogProps, ContextMenu, type ContextMenuItem, type ContextMenuProps, DatePicker, type DatePickerProps, DateRangePicker, type DateRangePickerProps, type Density, DensityContext, type DensityContextValue, DensityProvider, Drawer, type DrawerProps, type DrawerSide, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, type EmptyStateProps, FileDropzone, type FileDropzoneLabels, type FileDropzoneProps, type FileDropzoneValue, FormField, type FormFieldProps, IconButton, type IconButtonProps, Popover, type PopoverAlign, type PopoverProps, SelectField, type SelectFieldProps, SettingsPanel, type SettingsPanelProps, Skeleton, type SkeletonProps, StatCard, type StatCardProps, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type Theme, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThemeSwitcher, type ThemeSwitcherProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineItemStatus, type TimelineItemTone, type TimelineOrientation, type TimelineProps, type TimelineVariant, Toggle, type ToggleProps, type UiStrings, UiStringsProvider, type UiStringsProviderProps, type UseFloatingPanelOptions, type UseFloatingPanelResult, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
package/dist/index.d.mts CHANGED
@@ -204,6 +204,56 @@ interface TextAreaFieldProps extends TextareaHTMLAttributes<HTMLTextAreaElement>
204
204
  }
205
205
  declare const TextAreaField: import("react").ForwardRefExoticComponent<TextAreaFieldProps & import("react").RefAttributes<HTMLTextAreaElement>>;
206
206
  //#endregion
207
+ //#region packages/ui/FileDropzone.d.ts
208
+ interface FileDropzoneValue {
209
+ fileName?: string | null;
210
+ fileUrl?: string | null;
211
+ previewUrl?: string | null;
212
+ }
213
+ interface FileDropzoneLabels {
214
+ prompt?: string;
215
+ imagePrompt?: string;
216
+ dropPrompt?: string;
217
+ hint?: string;
218
+ imageHint?: string;
219
+ uploading?: string;
220
+ replace?: string;
221
+ remove?: string;
222
+ open?: string;
223
+ }
224
+ interface FileDropzoneProps {
225
+ accept?: string | null;
226
+ value?: FileDropzoneValue | null;
227
+ image?: boolean;
228
+ disabled?: boolean;
229
+ readOnly?: boolean;
230
+ invalid?: boolean;
231
+ uploading?: boolean;
232
+ error?: string | null;
233
+ labels?: FileDropzoneLabels;
234
+ inputId?: string;
235
+ inputLabel?: string;
236
+ className?: string;
237
+ onFileSelect: (file: File) => void;
238
+ onClear?: () => void;
239
+ }
240
+ declare function FileDropzone({
241
+ accept,
242
+ value,
243
+ image,
244
+ disabled,
245
+ readOnly,
246
+ invalid,
247
+ uploading,
248
+ error,
249
+ labels,
250
+ inputId,
251
+ inputLabel,
252
+ className,
253
+ onFileSelect,
254
+ onClear
255
+ }: FileDropzoneProps): import("react").JSX.Element;
256
+ //#endregion
207
257
  //#region packages/ui/AppDropdown.d.ts
208
258
  interface AppDropdownOption {
209
259
  value: string;
@@ -739,6 +789,8 @@ interface TimelineProps {
739
789
  interface TimelineItemProps {
740
790
  status: TimelineItemStatus;
741
791
  title: ReactNode;
792
+ /** Custom stepper marker content, for example a step number. Ignored by the `log` variant. */
793
+ marker?: ReactNode;
742
794
  timestamp?: ReactNode;
743
795
  dateTime?: string;
744
796
  /** Marker accent for the `log` variant. */
@@ -749,6 +801,7 @@ interface TimelineItemProps {
749
801
  declare function TimelineItem({
750
802
  status,
751
803
  title,
804
+ marker,
752
805
  timestamp,
753
806
  dateTime,
754
807
  tone,
@@ -826,4 +879,4 @@ interface UiStringsProviderProps {
826
879
  declare const UiStringsProvider: React$1.FC<UiStringsProviderProps>;
827
880
  declare const useUiStrings: () => UiStrings;
828
881
  //#endregion
829
- export { ACCENT_PRESETS, type AccentPreset, AppDialog, type AppDialogProps, AppDropdown, type AppDropdownOption, type AppDropdownProps, type AppDropdownVariant, AppToolbar, type AppToolbarProps, Avatar, type AvatarProps, type AvatarShape, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Chip, type ChipProps, CollapsibleSection, type CollapsibleSectionProps, ConfirmDialog, type ConfirmDialogProps, ContextMenu, type ContextMenuItem, type ContextMenuProps, DatePicker, type DatePickerProps, DateRangePicker, type DateRangePickerProps, type Density, DensityContext, type DensityContextValue, DensityProvider, Drawer, type DrawerProps, type DrawerSide, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, IconButton, type IconButtonProps, Popover, type PopoverAlign, type PopoverProps, SelectField, type SelectFieldProps, SettingsPanel, type SettingsPanelProps, Skeleton, type SkeletonProps, StatCard, type StatCardProps, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type Theme, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThemeSwitcher, type ThemeSwitcherProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineItemStatus, type TimelineItemTone, type TimelineOrientation, type TimelineProps, type TimelineVariant, Toggle, type ToggleProps, type UiStrings, UiStringsProvider, type UiStringsProviderProps, type UseFloatingPanelOptions, type UseFloatingPanelResult, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
882
+ export { ACCENT_PRESETS, type AccentPreset, AppDialog, type AppDialogProps, AppDropdown, type AppDropdownOption, type AppDropdownProps, type AppDropdownVariant, AppToolbar, type AppToolbarProps, Avatar, type AvatarProps, type AvatarShape, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Chip, type ChipProps, CollapsibleSection, type CollapsibleSectionProps, ConfirmDialog, type ConfirmDialogProps, ContextMenu, type ContextMenuItem, type ContextMenuProps, DatePicker, type DatePickerProps, DateRangePicker, type DateRangePickerProps, type Density, DensityContext, type DensityContextValue, DensityProvider, Drawer, type DrawerProps, type DrawerSide, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, type EmptyStateProps, FileDropzone, type FileDropzoneLabels, type FileDropzoneProps, type FileDropzoneValue, FormField, type FormFieldProps, IconButton, type IconButtonProps, Popover, type PopoverAlign, type PopoverProps, SelectField, type SelectFieldProps, SettingsPanel, type SettingsPanelProps, Skeleton, type SkeletonProps, StatCard, type StatCardProps, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type Theme, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThemeSwitcher, type ThemeSwitcherProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineItemStatus, type TimelineItemTone, type TimelineOrientation, type TimelineProps, type TimelineVariant, Toggle, type ToggleProps, type UiStrings, UiStringsProvider, type UiStringsProviderProps, type UseFloatingPanelOptions, type UseFloatingPanelResult, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { createPortal } from "react-dom";
4
+ import { useDropzone } from "react-dropzone";
4
5
  //#region packages/ui/Avatar.tsx
5
6
  /**
6
7
  * 8 semantic hues aligned with the Fluent / @nubitio design palette.
@@ -59,13 +60,13 @@ const Avatar = ({ owner = "", size, variant = "md", shape = "circle", alt, class
59
60
  };
60
61
  //#endregion
61
62
  //#region packages/ui/Button.tsx
62
- const cx$4 = (...values) => values.filter(Boolean).join(" ");
63
+ const cx$5 = (...values) => values.filter(Boolean).join(" ");
63
64
  const Button = ({ variant = "secondary", size = "md", fullWidth = false, icon, loading = false, className, children, disabled, ...props }) => /* @__PURE__ */ jsxs("button", {
64
65
  ...props,
65
66
  type: props.type ?? "button",
66
67
  disabled: disabled || loading,
67
68
  "aria-busy": loading || void 0,
68
- className: cx$4("nb-button", `nb-button--${variant}`, size === "sm" && "nb-button--sm", fullWidth && "nb-button--full", className),
69
+ className: cx$5("nb-button", `nb-button--${variant}`, size === "sm" && "nb-button--sm", fullWidth && "nb-button--full", className),
69
70
  children: [loading ? /* @__PURE__ */ jsx("span", {
70
71
  className: "nb-button-spinner",
71
72
  "aria-hidden": "true"
@@ -79,7 +80,7 @@ const IconButton = ({ icon, label, variant = "default", className, ...props }) =
79
80
  type: props.type ?? "button",
80
81
  "aria-label": props["aria-label"] ?? label,
81
82
  title: props.title ?? label,
82
- className: cx$4("nb-icon-button", variant === "danger" && "nb-icon-button--danger", className),
83
+ className: cx$5("nb-icon-button", variant === "danger" && "nb-icon-button--danger", className),
83
84
  children: /* @__PURE__ */ jsx("i", {
84
85
  className: icon,
85
86
  "aria-hidden": "true"
@@ -328,10 +329,10 @@ const Card = ({ title, description, children }) => {
328
329
  };
329
330
  //#endregion
330
331
  //#region packages/ui/FormControls.tsx
331
- const cx$3 = (...values) => values.filter(Boolean).join(" ");
332
+ const cx$4 = (...values) => values.filter(Boolean).join(" ");
332
333
  const FormField = ({ label, error, helpText, children, className, ...props }) => /* @__PURE__ */ jsxs("label", {
333
334
  ...props,
334
- className: cx$3("nb-form-field", !!error && "nb-form-field--error", className),
335
+ className: cx$4("nb-form-field", !!error && "nb-form-field--error", className),
335
336
  children: [
336
337
  label && /* @__PURE__ */ jsx("span", {
337
338
  className: "nb-form-field__label",
@@ -352,14 +353,14 @@ const TextField = forwardRef(function TextField({ className, invalid, ...props }
352
353
  return /* @__PURE__ */ jsx("input", {
353
354
  ...props,
354
355
  ref,
355
- className: cx$3("nb-input", invalid && "nb-input--invalid", className)
356
+ className: cx$4("nb-input", invalid && "nb-input--invalid", className)
356
357
  });
357
358
  });
358
359
  const SelectField = forwardRef(function SelectField({ className, invalid, children, ...props }, ref) {
359
360
  return /* @__PURE__ */ jsx("select", {
360
361
  ...props,
361
362
  ref,
362
- className: cx$3("nb-input", "nb-select", invalid && "nb-input--invalid", className),
363
+ className: cx$4("nb-input", "nb-select", invalid && "nb-input--invalid", className),
363
364
  children
364
365
  });
365
366
  });
@@ -367,10 +368,151 @@ const TextAreaField = forwardRef(function TextAreaField({ className, invalid, ..
367
368
  return /* @__PURE__ */ jsx("textarea", {
368
369
  ...props,
369
370
  ref,
370
- className: cx$3("nb-input", "nb-textarea", invalid && "nb-input--invalid", className)
371
+ className: cx$4("nb-input", "nb-textarea", invalid && "nb-input--invalid", className)
371
372
  });
372
373
  });
373
374
  //#endregion
375
+ //#region packages/ui/FileDropzone.tsx
376
+ function cx$3(...values) {
377
+ return values.filter(Boolean).join(" ");
378
+ }
379
+ function buildDropzoneAccept(accept) {
380
+ if (!accept || accept === "*/*" || accept === "*") return void 0;
381
+ if (accept === "image/*") return {
382
+ "image/png": [".png"],
383
+ "image/jpeg": [".jpg", ".jpeg"],
384
+ "image/webp": [".webp"],
385
+ "image/gif": [".gif"]
386
+ };
387
+ if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
388
+ const trimmed = token.trim();
389
+ if (!trimmed) return acc;
390
+ if (trimmed.startsWith(".")) {
391
+ acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
392
+ return acc;
393
+ }
394
+ acc[trimmed] = [];
395
+ return acc;
396
+ }, {});
397
+ if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
398
+ return { [accept]: [] };
399
+ }
400
+ function FileDropzone({ accept, value = null, image = false, disabled = false, readOnly = false, invalid = false, uploading = false, error = null, labels, inputId, inputLabel, className, onFileSelect, onClear }) {
401
+ const previewUrl = value?.previewUrl ?? null;
402
+ const fileName = value?.fileName ?? null;
403
+ const fileUrl = value?.fileUrl ?? null;
404
+ const hasContent = !!(previewUrl || fileName);
405
+ const isInteractive = !disabled && !readOnly;
406
+ const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
407
+ accept: buildDropzoneAccept(accept),
408
+ disabled: disabled || readOnly || uploading,
409
+ multiple: false,
410
+ noClick: hasContent,
411
+ noKeyboard: hasContent,
412
+ onDrop: (acceptedFiles) => {
413
+ const file = acceptedFiles[0];
414
+ if (file) onFileSelect(file);
415
+ }
416
+ });
417
+ const placeholderIcon = image ? "ph-image" : "ph-file-arrow-up";
418
+ const placeholderTitle = isDragActive ? labels?.dropPrompt ?? "Drop file here" : image ? labels?.imagePrompt ?? "Drop image here or click to browse" : labels?.prompt ?? "Drop file here or click to browse";
419
+ const placeholderHint = image ? labels?.imageHint ?? "PNG, JPG, WebP or GIF" : labels?.hint ?? "Select one file";
420
+ return /* @__PURE__ */ jsxs("div", {
421
+ className: cx$3("nb-file-dropzone", image && "nb-file-dropzone--image", invalid && "nb-file-dropzone--invalid", className),
422
+ children: [/* @__PURE__ */ jsxs("div", {
423
+ ...getRootProps({ className: cx$3("nb-file-dropzone__zone", isDragActive && "nb-file-dropzone__zone--active", hasContent && "nb-file-dropzone__zone--filled", uploading && "nb-file-dropzone__zone--uploading", !isInteractive && "nb-file-dropzone__zone--disabled") }),
424
+ children: [/* @__PURE__ */ jsx("input", { ...getInputProps({
425
+ id: inputId,
426
+ "aria-label": inputLabel
427
+ }) }), hasContent ? /* @__PURE__ */ jsxs(Fragment, { children: [
428
+ previewUrl ? /* @__PURE__ */ jsx("img", {
429
+ className: "nb-file-dropzone__preview",
430
+ src: previewUrl,
431
+ alt: inputLabel ?? fileName ?? ""
432
+ }) : /* @__PURE__ */ jsxs("div", {
433
+ className: "nb-file-dropzone__file",
434
+ children: [/* @__PURE__ */ jsx("span", {
435
+ className: "nb-file-dropzone__file-icon",
436
+ "aria-hidden": "true",
437
+ children: /* @__PURE__ */ jsx("i", { className: "ph ph-file" })
438
+ }), /* @__PURE__ */ jsxs("div", {
439
+ className: "nb-file-dropzone__file-meta",
440
+ children: [/* @__PURE__ */ jsx("span", {
441
+ className: "nb-file-dropzone__file-name",
442
+ children: fileName
443
+ }), fileUrl && /* @__PURE__ */ jsx("a", {
444
+ className: "nb-file-dropzone__file-link",
445
+ href: fileUrl,
446
+ target: "_blank",
447
+ rel: "noreferrer",
448
+ onClick: (event) => event.stopPropagation(),
449
+ children: labels?.open ?? "Open"
450
+ })]
451
+ })]
452
+ }),
453
+ uploading && /* @__PURE__ */ jsxs("div", {
454
+ className: "nb-file-dropzone__overlay",
455
+ "aria-live": "polite",
456
+ children: [/* @__PURE__ */ jsx("span", {
457
+ className: "nb-file-dropzone__spinner",
458
+ "aria-hidden": "true"
459
+ }), /* @__PURE__ */ jsx("span", { children: labels?.uploading ?? "Uploading..." })]
460
+ }),
461
+ isInteractive && !uploading && /* @__PURE__ */ jsxs("div", {
462
+ className: "nb-file-dropzone__actions",
463
+ children: [/* @__PURE__ */ jsxs("button", {
464
+ type: "button",
465
+ className: "nb-file-dropzone__action",
466
+ onClick: (event) => {
467
+ event.stopPropagation();
468
+ open();
469
+ },
470
+ children: [/* @__PURE__ */ jsx("i", {
471
+ className: "ph ph-arrows-clockwise",
472
+ "aria-hidden": "true"
473
+ }), labels?.replace ?? "Replace"]
474
+ }), onClear && /* @__PURE__ */ jsxs("button", {
475
+ type: "button",
476
+ className: "nb-file-dropzone__action nb-file-dropzone__action--danger",
477
+ onClick: (event) => {
478
+ event.stopPropagation();
479
+ onClear();
480
+ },
481
+ children: [/* @__PURE__ */ jsx("i", {
482
+ className: "ph ph-trash",
483
+ "aria-hidden": "true"
484
+ }), labels?.remove ?? "Remove"]
485
+ })]
486
+ })
487
+ ] }) : /* @__PURE__ */ jsxs("div", {
488
+ className: "nb-file-dropzone__placeholder",
489
+ children: [
490
+ /* @__PURE__ */ jsx("span", {
491
+ className: "nb-file-dropzone__icon",
492
+ "aria-hidden": "true",
493
+ children: /* @__PURE__ */ jsx("i", { className: `ph ${placeholderIcon}` })
494
+ }),
495
+ /* @__PURE__ */ jsx("span", {
496
+ className: "nb-file-dropzone__title",
497
+ children: placeholderTitle
498
+ }),
499
+ /* @__PURE__ */ jsx("span", {
500
+ className: "nb-file-dropzone__hint",
501
+ children: placeholderHint
502
+ })
503
+ ]
504
+ })]
505
+ }), error && /* @__PURE__ */ jsxs("span", {
506
+ className: "nb-file-dropzone__error",
507
+ role: "alert",
508
+ children: [/* @__PURE__ */ jsx("i", {
509
+ className: "ph ph-warning-circle",
510
+ "aria-hidden": "true"
511
+ }), error]
512
+ })]
513
+ });
514
+ }
515
+ //#endregion
374
516
  //#region packages/ui/AppDropdown.tsx
375
517
  function cx$2(...values) {
376
518
  return values.filter(Boolean).join(" ");
@@ -2334,11 +2476,16 @@ const TimelineVariantContext = createContext("stepper");
2334
2476
  function useTimelineVariant() {
2335
2477
  return useContext(TimelineVariantContext);
2336
2478
  }
2337
- function TimelineMarker({ status, variant }) {
2479
+ function TimelineMarker({ status, variant, marker }) {
2338
2480
  if (variant === "log") return /* @__PURE__ */ jsx("span", {
2339
2481
  className: "nb-timeline__marker nb-timeline__marker--dot",
2340
2482
  "aria-hidden": "true"
2341
2483
  });
2484
+ if (marker != null) return /* @__PURE__ */ jsx("span", {
2485
+ className: `nb-timeline__marker nb-timeline__marker--${status} nb-timeline__marker--custom`,
2486
+ "aria-hidden": "true",
2487
+ children: marker
2488
+ });
2342
2489
  if (status === "complete") return /* @__PURE__ */ jsx("span", {
2343
2490
  className: "nb-timeline__marker nb-timeline__marker--complete",
2344
2491
  "aria-hidden": "true",
@@ -2358,7 +2505,7 @@ function TimelineMarker({ status, variant }) {
2358
2505
  "aria-hidden": "true"
2359
2506
  });
2360
2507
  }
2361
- function TimelineItem({ status, title, timestamp, dateTime, tone = "default", children, className }) {
2508
+ function TimelineItem({ status, title, marker, timestamp, dateTime, tone = "default", children, className }) {
2362
2509
  const variant = useTimelineVariant();
2363
2510
  return /* @__PURE__ */ jsxs("li", {
2364
2511
  className: [
@@ -2371,7 +2518,8 @@ function TimelineItem({ status, title, timestamp, dateTime, tone = "default", ch
2371
2518
  className: "nb-timeline__marker-col",
2372
2519
  children: /* @__PURE__ */ jsx(TimelineMarker, {
2373
2520
  status,
2374
- variant
2521
+ variant,
2522
+ marker
2375
2523
  })
2376
2524
  }), /* @__PURE__ */ jsxs("div", {
2377
2525
  className: "nb-timeline__content",
@@ -2419,4 +2567,4 @@ function Timeline({ variant = "stepper", orientation = "vertical", title, descri
2419
2567
  });
2420
2568
  }
2421
2569
  //#endregion
2422
- export { ACCENT_PRESETS, AppDialog, AppDropdown, AppToolbar, Avatar, Badge, Button, Card, Chip, CollapsibleSection, ConfirmDialog, ContextMenu, DatePicker, DateRangePicker, DensityContext, DensityProvider, Drawer, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, FormField, IconButton, Popover, SelectField, SettingsPanel, Skeleton, StatCard, TextAreaField, TextField, ThemeContext, ThemeProvider, ThemeSwitcher, Timeline, TimelineItem, Toggle, UiStringsProvider, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
2570
+ export { ACCENT_PRESETS, AppDialog, AppDropdown, AppToolbar, Avatar, Badge, Button, Card, Chip, CollapsibleSection, ConfirmDialog, ContextMenu, DatePicker, DateRangePicker, DensityContext, DensityProvider, Drawer, EN_UI_STRINGS, ES_UI_STRINGS, EmptyState, FileDropzone, FormField, IconButton, Popover, SelectField, SettingsPanel, Skeleton, StatCard, TextAreaField, TextField, ThemeContext, ThemeProvider, ThemeSwitcher, Timeline, TimelineItem, Toggle, UiStringsProvider, getAvatarHue, getAvatarInitials, useAccentColor, useDensity, useFloatingPanel, useTheme, useUiStrings };
package/dist/style.css CHANGED
@@ -753,6 +753,238 @@ html[data-density=compact] .nb-dialog__footer {
753
753
  padding: var(--space-2) var(--space-3);
754
754
  resize: vertical;
755
755
  }
756
+ .nb-file-dropzone {
757
+ display: flex;
758
+ flex-direction: column;
759
+ gap: var(--space-1);
760
+ width: 100%;
761
+ }
762
+
763
+ .nb-file-dropzone__zone {
764
+ align-items: center;
765
+ background: var(--surface-1);
766
+ border: 1px dashed var(--border-color);
767
+ border-radius: var(--radius-lg);
768
+ box-sizing: border-box;
769
+ cursor: pointer;
770
+ display: flex;
771
+ justify-content: center;
772
+ min-height: 112px;
773
+ overflow: hidden;
774
+ position: relative;
775
+ transition: border-color var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
776
+ width: 100%;
777
+ }
778
+ .nb-file-dropzone__zone:hover:not(.nb-file-dropzone__zone--disabled):not(.nb-file-dropzone__zone--filled) {
779
+ background: color-mix(in srgb, var(--accent-color) 4%, var(--surface-1));
780
+ border-color: var(--accent-color);
781
+ }
782
+ .nb-file-dropzone__zone--active {
783
+ background: color-mix(in srgb, var(--accent-color) 8%, var(--surface-1));
784
+ border-color: var(--accent-color);
785
+ }
786
+ .nb-file-dropzone__zone--filled {
787
+ border-style: solid;
788
+ cursor: default;
789
+ min-height: 88px;
790
+ }
791
+ .nb-file-dropzone__zone--uploading {
792
+ pointer-events: none;
793
+ }
794
+ .nb-file-dropzone__zone--disabled {
795
+ cursor: not-allowed;
796
+ opacity: 0.72;
797
+ }
798
+ .nb-file-dropzone__zone:focus-visible {
799
+ box-shadow: 0 0 0 3px var(--focus-ring-color);
800
+ outline: none;
801
+ }
802
+
803
+ .nb-file-dropzone--image .nb-file-dropzone__zone {
804
+ min-height: 168px;
805
+ }
806
+
807
+ .nb-file-dropzone--image .nb-file-dropzone__zone--filled {
808
+ min-height: 180px;
809
+ }
810
+
811
+ .nb-file-dropzone--invalid .nb-file-dropzone__zone {
812
+ border-color: var(--error-color);
813
+ }
814
+
815
+ .nb-file-dropzone__placeholder {
816
+ align-items: center;
817
+ display: flex;
818
+ flex-direction: column;
819
+ gap: var(--space-2);
820
+ max-width: 320px;
821
+ padding: var(--space-4);
822
+ text-align: center;
823
+ }
824
+
825
+ .nb-file-dropzone__icon {
826
+ align-items: center;
827
+ background: color-mix(in srgb, var(--accent-color) 10%, transparent);
828
+ border-radius: 999px;
829
+ color: var(--accent-color);
830
+ display: inline-flex;
831
+ font-size: 24px;
832
+ height: 48px;
833
+ justify-content: center;
834
+ width: 48px;
835
+ }
836
+
837
+ .nb-file-dropzone__title {
838
+ color: var(--text-primary);
839
+ font-size: var(--font-size-sm);
840
+ font-weight: var(--font-weight-semibold);
841
+ }
842
+
843
+ .nb-file-dropzone__hint {
844
+ color: var(--text-tertiary);
845
+ font-size: var(--font-size-xs);
846
+ line-height: var(--line-height-tight);
847
+ }
848
+
849
+ .nb-file-dropzone__preview {
850
+ display: block;
851
+ height: 100%;
852
+ max-height: 220px;
853
+ object-fit: contain;
854
+ width: 100%;
855
+ }
856
+
857
+ .nb-file-dropzone__file {
858
+ align-items: center;
859
+ display: flex;
860
+ gap: var(--space-3);
861
+ max-width: 100%;
862
+ padding: var(--space-3) var(--space-4);
863
+ width: 100%;
864
+ }
865
+
866
+ .nb-file-dropzone__file-icon {
867
+ align-items: center;
868
+ background: var(--surface-0);
869
+ border: 1px solid var(--border-subtle);
870
+ border-radius: var(--radius-md);
871
+ color: var(--accent-color);
872
+ display: inline-flex;
873
+ flex: 0 0 auto;
874
+ font-size: 22px;
875
+ height: 44px;
876
+ justify-content: center;
877
+ width: 44px;
878
+ }
879
+
880
+ .nb-file-dropzone__file-meta {
881
+ display: flex;
882
+ flex: 1 1 auto;
883
+ flex-direction: column;
884
+ gap: 2px;
885
+ min-width: 0;
886
+ }
887
+
888
+ .nb-file-dropzone__file-name {
889
+ color: var(--text-primary);
890
+ font-size: var(--font-size-sm);
891
+ font-weight: var(--font-weight-medium);
892
+ overflow: hidden;
893
+ text-overflow: ellipsis;
894
+ white-space: nowrap;
895
+ }
896
+
897
+ .nb-file-dropzone__file-link {
898
+ color: var(--accent-color);
899
+ font-size: var(--font-size-xs);
900
+ text-decoration: none;
901
+ }
902
+ .nb-file-dropzone__file-link:hover {
903
+ text-decoration: underline;
904
+ }
905
+
906
+ .nb-file-dropzone__overlay {
907
+ align-items: center;
908
+ background: rgba(0, 0, 0, 0.42);
909
+ color: #fff;
910
+ display: flex;
911
+ flex-direction: column;
912
+ font-size: var(--font-size-sm);
913
+ gap: var(--space-2);
914
+ inset: 0;
915
+ justify-content: center;
916
+ position: absolute;
917
+ }
918
+
919
+ .nb-file-dropzone__spinner {
920
+ animation: nb-file-dropzone-spin 700ms linear infinite;
921
+ border: 2px solid rgba(255, 255, 255, 0.35);
922
+ border-radius: 999px;
923
+ border-top-color: #fff;
924
+ height: 24px;
925
+ width: 24px;
926
+ }
927
+
928
+ @keyframes nb-file-dropzone-spin {
929
+ to {
930
+ transform: rotate(360deg);
931
+ }
932
+ }
933
+ .nb-file-dropzone__actions {
934
+ align-items: center;
935
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.58), transparent);
936
+ bottom: 0;
937
+ display: flex;
938
+ gap: var(--space-2);
939
+ inset-inline: 0;
940
+ justify-content: center;
941
+ opacity: 0;
942
+ padding: var(--space-3);
943
+ position: absolute;
944
+ transition: opacity var(--transition-base);
945
+ }
946
+
947
+ .nb-file-dropzone__zone--filled:hover .nb-file-dropzone__actions,
948
+ .nb-file-dropzone__zone--filled:focus-within .nb-file-dropzone__actions {
949
+ opacity: 1;
950
+ }
951
+
952
+ .nb-file-dropzone__action {
953
+ align-items: center;
954
+ background: var(--surface-1);
955
+ border: 1px solid var(--border-subtle);
956
+ border-radius: var(--radius-md);
957
+ color: var(--text-primary);
958
+ cursor: pointer;
959
+ display: inline-flex;
960
+ font: inherit;
961
+ font-size: var(--font-size-xs);
962
+ font-weight: var(--font-weight-medium);
963
+ gap: var(--space-1);
964
+ min-height: 28px;
965
+ padding: 0 var(--space-2);
966
+ transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
967
+ }
968
+ .nb-file-dropzone__action:hover {
969
+ border-color: var(--accent-color);
970
+ color: var(--accent-color);
971
+ }
972
+ .nb-file-dropzone__action--danger:hover {
973
+ border-color: var(--error-color);
974
+ color: var(--error-color);
975
+ }
976
+ .nb-file-dropzone__action:focus-visible {
977
+ box-shadow: 0 0 0 2px var(--focus-ring-color);
978
+ outline: none;
979
+ }
980
+
981
+ .nb-file-dropzone__error {
982
+ align-items: center;
983
+ color: var(--error-color);
984
+ display: inline-flex;
985
+ font-size: var(--font-size-xs);
986
+ gap: var(--space-1);
987
+ }
756
988
  .nb-dropdown {
757
989
  min-width: 0;
758
990
  position: relative;
@@ -2827,6 +3059,15 @@ html[data-density=compact] .nb-drawer__footer {
2827
3059
  width: 20px;
2828
3060
  }
2829
3061
 
3062
+ .nb-timeline__marker--custom {
3063
+ font-size: var(--font-size-xs);
3064
+ font-weight: var(--font-weight-semibold);
3065
+ line-height: 1;
3066
+ }
3067
+ .nb-timeline__marker--custom.nb-timeline__marker--current::after {
3068
+ display: none;
3069
+ }
3070
+
2830
3071
  .nb-timeline__marker--dot {
2831
3072
  background: var(--surface-2);
2832
3073
  border: 2px solid var(--accent-color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/ui",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "type": "module",
5
5
  "description": "Visual primitives and theme system for the Nubit admin stack (dialogs, cards, toolbar, light/dark theme).",
6
6
  "license": "MIT",
@@ -52,5 +52,8 @@
52
52
  "peerDependencies": {
53
53
  "react": "^19.0.0",
54
54
  "react-dom": "^19.0.0"
55
+ },
56
+ "dependencies": {
57
+ "react-dropzone": "^15.0.0"
55
58
  }
56
59
  }