aplance-ui 1.0.1

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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/dist/api.d.ts +175 -0
  4. package/dist/api.js +1954 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/frameworks/astro/astro.config.snippet.md +23 -0
  7. package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
  8. package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
  9. package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
  10. package/dist/frameworks/next/layout-import.snippet.md +19 -0
  11. package/dist/frameworks/next/next.config.snippet.md +15 -0
  12. package/dist/frameworks/next/postcss.config.js.template +16 -0
  13. package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
  14. package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
  15. package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
  16. package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
  17. package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
  18. package/dist/index.js +2047 -0
  19. package/dist/index.js.map +1 -0
  20. package/package.json +63 -0
  21. package/registry/components/primitives/icon/icon.module.css +63 -0
  22. package/registry/components/primitives/icon/icon.tsx +53 -0
  23. package/registry/components/primitives/modal/modal-context.ts +19 -0
  24. package/registry/components/primitives/modal/modal.module.css +415 -0
  25. package/registry/components/primitives/modal/modal.tsx +550 -0
  26. package/registry/components/primitives/popover/popover-context.ts +28 -0
  27. package/registry/components/primitives/popover/popover.module.css +28 -0
  28. package/registry/components/primitives/popover/popover.tsx +465 -0
  29. package/registry/components/ui/accordion/accordion.module.css +208 -0
  30. package/registry/components/ui/accordion/accordion.tsx +327 -0
  31. package/registry/components/ui/avatar/avatar.module.css +32 -0
  32. package/registry/components/ui/avatar/avatar.tsx +37 -0
  33. package/registry/components/ui/badge/badge.module.css +103 -0
  34. package/registry/components/ui/badge/badge.tsx +29 -0
  35. package/registry/components/ui/button/button.module.css +216 -0
  36. package/registry/components/ui/button/button.tsx +133 -0
  37. package/registry/components/ui/chart/chart.module.css +168 -0
  38. package/registry/components/ui/chart/chart.tsx +1293 -0
  39. package/registry/components/ui/chart/types.ts +210 -0
  40. package/registry/components/ui/chart/utils.ts +59 -0
  41. package/registry/components/ui/copy-button/copy-button.module.css +21 -0
  42. package/registry/components/ui/copy-button/copy-button.tsx +123 -0
  43. package/registry/components/ui/countdown/countdown.module.css +5 -0
  44. package/registry/components/ui/countdown/countdown.tsx +152 -0
  45. package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
  46. package/registry/components/ui/data-table/data-table.module.css +594 -0
  47. package/registry/components/ui/data-table/data-table.tsx +1109 -0
  48. package/registry/components/ui/data-table/filter-bar.tsx +33 -0
  49. package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
  50. package/registry/components/ui/data-table/types.ts +594 -0
  51. package/registry/components/ui/data-table/useDataTable.tsx +573 -0
  52. package/registry/components/ui/data-table/utils.ts +445 -0
  53. package/registry/components/ui/empty-state/empty-state.module.css +26 -0
  54. package/registry/components/ui/empty-state/empty-state.tsx +32 -0
  55. package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
  56. package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
  57. package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
  58. package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
  59. package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
  60. package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
  61. package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
  62. package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
  63. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
  64. package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
  65. package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
  66. package/registry/components/ui/input-fields/common/types.ts +38 -0
  67. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
  68. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
  69. package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
  70. package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
  71. package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
  72. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
  73. package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
  74. package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
  75. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
  76. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
  77. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
  78. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
  79. package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
  80. package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
  81. package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
  82. package/registry/components/ui/input-fields/input/input.tsx +319 -0
  83. package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
  84. package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
  85. package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
  86. package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
  87. package/registry/components/ui/input-fields/media-input/types.ts +129 -0
  88. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
  89. package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
  90. package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
  91. package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
  92. package/registry/components/ui/input-fields/select/select.tsx +118 -0
  93. package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
  94. package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
  95. package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
  96. package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
  97. package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
  98. package/registry/components/ui/loader/loader.module.css +156 -0
  99. package/registry/components/ui/loader/loader.tsx +102 -0
  100. package/registry/components/ui/stepper/stepper.module.css +294 -0
  101. package/registry/components/ui/stepper/stepper.tsx +197 -0
  102. package/registry/components/ui/tabs/tabs.module.css +150 -0
  103. package/registry/components/ui/tabs/tabs.tsx +134 -0
  104. package/registry/components/ui/toast/toast.module.css +159 -0
  105. package/registry/components/ui/toast/toast.tsx +104 -0
  106. package/registry/components/ui/tooltip/tooltip.module.css +82 -0
  107. package/registry/components/ui/tooltip/tooltip.tsx +72 -0
  108. package/registry/global.d.ts +4 -0
  109. package/registry/hooks/useFocusOutside.tsx +33 -0
  110. package/registry/hooks/useResizeObserver.tsx +32 -0
  111. package/registry/lib/dom.ts +25 -0
  112. package/registry/lib/string.ts +23 -0
  113. package/registry/registry.json +719 -0
  114. package/registry/styles/app.css +240 -0
  115. package/registry/styles/components.css +78 -0
  116. package/registry/styles/fonts/geist-regular.woff2 +0 -0
  117. package/registry/styles/globals.css +7 -0
  118. package/registry/styles/media.css +25 -0
  119. package/registry/styles/utilities.css +90 -0
  120. package/registry/themes/colors/blue.css +37 -0
  121. package/registry/themes/colors/green.css +37 -0
  122. package/registry/themes/colors/rose.css +37 -0
  123. package/registry/themes/colors/slate.css +37 -0
  124. package/registry/themes/colors/stone.css +37 -0
  125. package/registry/themes/colors/violet.css +37 -0
  126. package/registry/themes/colors/zinc.css +37 -0
  127. package/registry/themes/radius/pill.css +8 -0
  128. package/registry/themes/radius/rounded.css +8 -0
  129. package/registry/themes/radius/sharp.css +8 -0
  130. package/registry/themes/spacing/compact.css +17 -0
  131. package/registry/themes/spacing/spacious.css +17 -0
  132. package/registry/tsconfig.json +25 -0
@@ -0,0 +1,327 @@
1
+ import { type MouseEvent, useId, useMemo } from "react";
2
+ import clsx from "clsx";
3
+ import { IconUpload, IconX } from "@tabler/icons-react";
4
+ import sCommon from "../common/input-field-common.module.css";
5
+ import sButton from "../../button/button.module.css";
6
+ import { FieldMessage } from "../common/field-message/field-message";
7
+ import { Icon } from "../../../primitives/icon/icon";
8
+ import { Button, type ButtonVariant } from "../../button/button";
9
+ import type { MediaInputProps } from "./types";
10
+ import { useMediaUpload } from "./useMediaUpload";
11
+ import { MediaPreview } from "./media-preview";
12
+ import { formatBytes } from "./utils";
13
+ import s from "./media-input.module.css";
14
+
15
+ /* This file is intentionally component-only (only exports `MediaInput`)
16
+ so Vite Fast Refresh can preserve component state across edits. Helpers,
17
+ hooks, types, and constants live in their own files and are re-exported
18
+ from the package index - see `packages/core/src/index.ts`. */
19
+
20
+ export const MediaInput = (props: MediaInputProps) => {
21
+ const {
22
+ label,
23
+ className,
24
+ style,
25
+ id,
26
+ name,
27
+ info,
28
+ errors,
29
+ layout = "default",
30
+ layoutDirection = "default",
31
+
32
+ accept,
33
+ multiple = false,
34
+ maxFiles,
35
+ maxSize,
36
+ disabled = false,
37
+ readOnly = false,
38
+ required = false,
39
+
40
+ buttonStyle = "none",
41
+ browseButtonVariant = "bordered",
42
+
43
+ showDropzone = true,
44
+ previewLayout = "grid",
45
+ previewSize = "md",
46
+ showRemove = true,
47
+ showClearAll = true,
48
+
49
+ dropzoneTitle,
50
+ dropzoneHint,
51
+ browseLabel,
52
+ dropzoneIcon,
53
+
54
+ renderPreview,
55
+ renderDropzone,
56
+
57
+ previewListClassName,
58
+ previewListStyle,
59
+
60
+ value,
61
+ defaultValue,
62
+ onChange,
63
+ onAccepted,
64
+ onRejected,
65
+ onRemove,
66
+ } = props;
67
+
68
+ const generatedId = useId();
69
+ const inputId = id ?? generatedId;
70
+ const labelId = `${inputId}-label`;
71
+ const hintId = `${inputId}-hint`;
72
+
73
+ const {
74
+ files,
75
+ rejections,
76
+ isDragging,
77
+ open,
78
+ remove,
79
+ clear,
80
+ getRootProps,
81
+ getInputProps,
82
+ } = useMediaUpload({
83
+ value,
84
+ defaultValue,
85
+ onChange,
86
+ onAccepted,
87
+ onRejected,
88
+ onRemove,
89
+ accept,
90
+ multiple,
91
+ maxFiles,
92
+ maxSize,
93
+ disabled,
94
+ readOnly,
95
+ });
96
+
97
+ const hasFiles = files.length > 0;
98
+ const interactive = !disabled && !readOnly;
99
+
100
+ // Merge rejection messages with consumer-supplied errors so the field message
101
+ // slot stays the single source of validation feedback.
102
+ const mergedErrors = useMemo(() => {
103
+ if (rejections.length === 0) return errors;
104
+ const next = errors ? [...errors] : [];
105
+ rejections.forEach((r) => next.push(r.message));
106
+ return next;
107
+ }, [errors, rejections]);
108
+
109
+ const resolvedTitle =
110
+ dropzoneTitle ??
111
+ (multiple ? "Drop files here or browse" : "Drop a file here or browse");
112
+ const resolvedHint =
113
+ dropzoneHint ??
114
+ [
115
+ accept ? `Accepts ${accept}` : null,
116
+ maxSize ? `Up to ${formatBytes(maxSize)} per file` : null,
117
+ multiple && maxFiles ? `Max ${maxFiles} files` : null,
118
+ ]
119
+ .filter(Boolean)
120
+ .join(" • ");
121
+
122
+ const inputProps = getInputProps({
123
+ id: inputId,
124
+ name,
125
+ required,
126
+ });
127
+
128
+ // Mirrors the SelectButton pattern from common/select-button:
129
+ // outer wrapper holds the variant CSS-variable contracts, inner element
130
+ // is either a default-styled drop target (when "none") or a `<Button as="div"
131
+ // variant=X>` whose full paint becomes the dropzone surface.
132
+ const hasButtonStyle = buttonStyle !== "none";
133
+ const buttonToneClass = hasButtonStyle
134
+ ? sButton[`tone_${buttonStyle}`]
135
+ : undefined;
136
+
137
+ const browseButton = browseButtonVariant !== "none" && interactive && (
138
+ <Button
139
+ type="button"
140
+ variant={browseButtonVariant as ButtonVariant}
141
+ size="sm"
142
+ onClick={(e: MouseEvent) => {
143
+ e.stopPropagation();
144
+ open();
145
+ }}
146
+ disabled={disabled}
147
+ >
148
+ {browseLabel ?? "Browse files"}
149
+ </Button>
150
+ );
151
+
152
+ const dropzoneNode = showDropzone ? (
153
+ renderDropzone ? (
154
+ renderDropzone({
155
+ isDragging,
156
+ open,
157
+ disabled,
158
+ readOnly,
159
+ hasFiles,
160
+ })
161
+ ) : (
162
+ <div className={s.dropzone_inner}>
163
+ <Icon size="md" className={s.dropzone_icon} aria-hidden="true">
164
+ {dropzoneIcon ?? <IconUpload />}
165
+ </Icon>
166
+ <div className={s.dropzone_copy}>
167
+ <span className={s.dropzone_title}>{resolvedTitle}</span>
168
+ {resolvedHint && (
169
+ <span id={hintId} className={s.dropzone_hint}>
170
+ {resolvedHint}
171
+ </span>
172
+ )}
173
+ </div>
174
+ {browseButton}
175
+ </div>
176
+ )
177
+ ) : null;
178
+
179
+ // Dropzone is non-selectable when a single-file picker already has its slot
180
+ // filled - remove the existing file first to add a different one.
181
+ const dropzoneSelectable = interactive && (multiple || !hasFiles);
182
+
183
+ const hasLabelAction = hasFiles && showClearAll && interactive;
184
+
185
+ const showLabelRow = Boolean(label) || hasLabelAction;
186
+
187
+ const dropzoneClassName = hasButtonStyle
188
+ ? clsx(
189
+ s.dropzone,
190
+ s.dropzone_button_shell,
191
+ sCommon.field_full_width,
192
+ sCommon.field_button_variant,
193
+ sButton.input_focus,
194
+ buttonToneClass,
195
+ isDragging && s.dropzone_active,
196
+ !dropzoneSelectable && s.dropzone_disabled,
197
+ )
198
+ : clsx(
199
+ s.dropzone,
200
+ s.dropzone_default,
201
+ isDragging && s.dropzone_active,
202
+ !dropzoneSelectable && s.dropzone_disabled,
203
+ );
204
+
205
+ const dropzoneRootProps = getRootProps<HTMLDivElement>({
206
+ className: dropzoneClassName,
207
+ role: "button",
208
+ tabIndex: dropzoneSelectable ? 0 : -1,
209
+ "aria-disabled": !dropzoneSelectable,
210
+ "aria-labelledby": label ? labelId : undefined,
211
+ "aria-describedby": resolvedHint ? hintId : undefined,
212
+ onClick: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
213
+ });
214
+
215
+ return (
216
+ <div className={clsx(sCommon.container, className)} style={style}>
217
+ {showLabelRow && (
218
+ <div
219
+ className={clsx(
220
+ sCommon.field_container,
221
+ layout !== "default" && sCommon[layout],
222
+ layout !== "default" &&
223
+ layoutDirection !== "default" &&
224
+ sCommon[layoutDirection],
225
+ )}
226
+ >
227
+ <div
228
+ className={clsx(
229
+ hasLabelAction && sCommon.label_row,
230
+ hasLabelAction && !label && sCommon.field_justify_end,
231
+ )}
232
+ >
233
+ {label && (
234
+ <label
235
+ id={labelId}
236
+ className={sCommon.label}
237
+ htmlFor={inputId}
238
+ >
239
+ {label}
240
+ </label>
241
+ )}
242
+ {hasLabelAction && (
243
+ <Button
244
+ type="button"
245
+ variant="ghost"
246
+ size="sm"
247
+ onClick={(e: MouseEvent) => {
248
+ e.stopPropagation();
249
+ clear();
250
+ }}
251
+ className={s.clear_all}
252
+ >
253
+ <Icon size="sm" aria-hidden="true">
254
+ <IconX />
255
+ </Icon>
256
+ Clear all
257
+ </Button>
258
+ )}
259
+ </div>
260
+ </div>
261
+ )}
262
+
263
+ <input {...inputProps} />
264
+
265
+ {showDropzone && (
266
+ <div
267
+ className={clsx(
268
+ s.dropzone_wrapper,
269
+ hasButtonStyle && sCommon.field_wrapper_variant,
270
+ hasButtonStyle && buttonToneClass,
271
+ )}
272
+ >
273
+ {hasButtonStyle ? (
274
+ <Button
275
+ as="div"
276
+ variant={buttonStyle as ButtonVariant}
277
+ isActive={isDragging}
278
+ disabled={disabled}
279
+ {...dropzoneRootProps}
280
+ >
281
+ {dropzoneNode}
282
+ </Button>
283
+ ) : (
284
+ <div {...dropzoneRootProps}>{dropzoneNode}</div>
285
+ )}
286
+ </div>
287
+ )}
288
+
289
+ <FieldMessage errors={mergedErrors} info={info} />
290
+
291
+ {hasFiles && previewLayout !== "none" && (
292
+ <ul
293
+ className={clsx(
294
+ s.preview_list,
295
+ s[`preview_list_${previewSize}`],
296
+ previewLayout === "list" && s.preview_list_layout_list,
297
+ previewListClassName,
298
+ )}
299
+ style={previewListStyle}
300
+ >
301
+ {files.map((file) => (
302
+ <li key={file.id} className={s.preview_item_wrap}>
303
+ {renderPreview ? (
304
+ renderPreview(file, {
305
+ remove: () => remove(file.id),
306
+ disabled,
307
+ readOnly,
308
+ size: previewSize,
309
+ })
310
+ ) : (
311
+ <MediaPreview
312
+ file={file}
313
+ layout={previewLayout}
314
+ size={previewSize}
315
+ showRemove={showRemove}
316
+ disabled={disabled}
317
+ readOnly={readOnly}
318
+ onRemove={() => remove(file.id)}
319
+ />
320
+ )}
321
+ </li>
322
+ ))}
323
+ </ul>
324
+ )}
325
+ </div>
326
+ );
327
+ };
@@ -0,0 +1,101 @@
1
+ import { type FC, type MouseEvent } from "react";
2
+ import clsx from "clsx";
3
+ import { IconX } from "@tabler/icons-react";
4
+ import { Icon } from "../../../primitives/icon/icon";
5
+ import { Button } from "../../button/button";
6
+ import type {
7
+ MediaFile,
8
+ MediaPreviewLayout,
9
+ MediaPreviewSize,
10
+ } from "./types";
11
+ import { formatBytes, previewKindOf } from "./utils";
12
+ import { KIND_LABEL, iconForKind } from "./kind-icons";
13
+ import s from "./media-input.module.css";
14
+
15
+ export type MediaPreviewProps = {
16
+ file: MediaFile;
17
+ layout: MediaPreviewLayout;
18
+ size: MediaPreviewSize;
19
+ showRemove: boolean;
20
+ disabled: boolean;
21
+ readOnly: boolean;
22
+ onRemove: () => void;
23
+ };
24
+
25
+ export const MediaPreview: FC<MediaPreviewProps> = ({
26
+ file,
27
+ layout,
28
+ size,
29
+ showRemove,
30
+ disabled,
31
+ readOnly,
32
+ onRemove,
33
+ }) => {
34
+ const kind = previewKindOf(file.file);
35
+ const showRemoveBtn = showRemove && !disabled && !readOnly;
36
+
37
+ const thumb =
38
+ kind === "image" && file.previewUrl ? (
39
+ <img
40
+ src={file.previewUrl}
41
+ alt={file.file.name}
42
+ className={s.preview_image}
43
+ draggable={false}
44
+ />
45
+ ) : kind === "video" && file.previewUrl ? (
46
+ <video
47
+ src={file.previewUrl}
48
+ className={s.preview_image}
49
+ muted
50
+ playsInline
51
+ />
52
+ ) : (
53
+ <div className={s.preview_thumb_fallback} aria-hidden="true">
54
+ <Icon size={layout === "list" ? "sm" : "md"}>{iconForKind(kind)}</Icon>
55
+ </div>
56
+ );
57
+
58
+ return (
59
+ <div
60
+ className={clsx(
61
+ s.preview_item,
62
+ layout === "list" ? s.preview_item_list : s[`preview_size_${size}`],
63
+ )}
64
+ data-status={file.status}
65
+ data-kind={kind}
66
+ >
67
+ <div className={s.preview_thumb}>{thumb}</div>
68
+
69
+ <div className={s.preview_meta}>
70
+ <span className={s.preview_name} title={file.file.name}>
71
+ {file.file.name}
72
+ </span>
73
+ <span className={s.preview_sub}>
74
+ <span>{KIND_LABEL[kind]}</span>
75
+ <span aria-hidden="true">·</span>
76
+ <span>{formatBytes(file.file.size)}</span>
77
+ </span>
78
+ {file.error && <span className={s.preview_error}>{file.error}</span>}
79
+ </div>
80
+
81
+ {showRemoveBtn && (
82
+ <Button
83
+ type="button"
84
+ variant="ghost"
85
+ size="sm"
86
+ layout="box"
87
+ className={s.preview_remove}
88
+ onClick={(e: MouseEvent) => {
89
+ e.stopPropagation();
90
+ onRemove();
91
+ }}
92
+ aria-label={`Remove ${file.file.name}`}
93
+ >
94
+ <Icon size="sm" aria-hidden="true">
95
+ <IconX />
96
+ </Icon>
97
+ </Button>
98
+ )}
99
+ </div>
100
+ );
101
+ };
@@ -0,0 +1,129 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { ButtonVariant } from "../../button/button";
3
+ import type { BaseInputFieldProps } from "../common/types";
4
+
5
+ /** Standardized rejection reasons surfaced to consumers and the field message slot. */
6
+ export const mediaRejectionCodes = [
7
+ "file-too-large",
8
+ "file-invalid-type",
9
+ "too-many-files",
10
+ "duplicate-file",
11
+ ] as const;
12
+ export type MediaRejectionCode = (typeof mediaRejectionCodes)[number];
13
+
14
+ export type MediaRejection = {
15
+ file: File;
16
+ code: MediaRejectionCode;
17
+ message: string;
18
+ };
19
+
20
+ /**
21
+ * Closed-set classification used to choose preview icons and labels. Always
22
+ * resolves to a value (never `undefined`) so renderers can rely on it.
23
+ */
24
+ export const mediaKinds = [
25
+ "image",
26
+ "video",
27
+ "audio",
28
+ "pdf",
29
+ "document",
30
+ "spreadsheet",
31
+ "presentation",
32
+ "archive",
33
+ "code",
34
+ "text",
35
+ "font",
36
+ "other",
37
+ ] as const;
38
+ export type MediaKind = (typeof mediaKinds)[number];
39
+
40
+ /**
41
+ * Stable wrapper around a File. The `id` is generated once when the file is
42
+ * accepted and stays the same across re-renders so previews and DOM keys
43
+ * remain consistent. `previewUrl` is an `URL.createObjectURL` blob that the
44
+ * hook revokes automatically when the file is removed or replaced.
45
+ */
46
+ export type MediaFile = {
47
+ id: string;
48
+ file: File;
49
+ previewUrl?: string;
50
+ status: "ready" | "uploading" | "uploaded" | "error";
51
+ progress?: number;
52
+ error?: string;
53
+ };
54
+
55
+ export const mediaPreviewLayouts = ["grid", "list", "none"] as const;
56
+ export type MediaPreviewLayout = (typeof mediaPreviewLayouts)[number];
57
+
58
+ export const mediaPreviewSizes = ["sm", "md", "lg"] as const;
59
+ export type MediaPreviewSize = (typeof mediaPreviewSizes)[number];
60
+
61
+ export type MediaInputProps = Omit<
62
+ BaseInputFieldProps,
63
+ "iconStart" | "iconEnd"
64
+ > & {
65
+ /** Native `accept` string (e.g. `image/*`, `image/png,application/pdf`). */
66
+ accept?: string;
67
+ /** Allow selecting multiple files. Default: `false`. */
68
+ multiple?: boolean;
69
+ /** Hard cap on accepted files (only enforced when `multiple`). */
70
+ maxFiles?: number;
71
+ /** Per-file size cap in bytes. */
72
+ maxSize?: number;
73
+
74
+ /**
75
+ * Variant for the inner Browse button. Independent from `buttonStyle`
76
+ * (which paints the dropzone surface). Set to `"none"` to hide the
77
+ * Browse button entirely. Default: `"bordered"`.
78
+ */
79
+ browseButtonVariant?: ButtonVariant | "none";
80
+
81
+ /** Show the dashed dropzone target. Default: `true`. */
82
+ showDropzone?: boolean;
83
+ /** How accepted files are rendered. `"none"` hides previews entirely. */
84
+ previewLayout?: MediaPreviewLayout;
85
+ /** Tile size for grid previews. Default: `"md"`. */
86
+ previewSize?: MediaPreviewSize;
87
+ /** Show the per-file remove (X) action on previews. Default: `true`. */
88
+ showRemove?: boolean;
89
+ /** Show a “Clear all” action when at least one file is selected. */
90
+ showClearAll?: boolean;
91
+
92
+ /** Copy slots - render-prop friendly so consumers can localize freely. */
93
+ dropzoneTitle?: ReactNode;
94
+ dropzoneHint?: ReactNode;
95
+ browseLabel?: ReactNode;
96
+ /** Replaces the icon shown inside the dropzone. */
97
+ dropzoneIcon?: ReactNode;
98
+
99
+ /** Render override for an individual preview tile. */
100
+ renderPreview?: (file: MediaFile, ctx: MediaPreviewContext) => ReactNode;
101
+ /** Render override for the entire dropzone body (keep wrapper styling). */
102
+ renderDropzone?: (ctx: MediaDropzoneContext) => ReactNode;
103
+
104
+ /** Pass-through styling for the inner preview list/grid. */
105
+ previewListClassName?: string;
106
+ previewListStyle?: CSSProperties;
107
+
108
+ value?: MediaFile[];
109
+ defaultValue?: MediaFile[] | File[];
110
+ onChange?: (files: MediaFile[]) => void;
111
+ onRemove?: (file: MediaFile) => void;
112
+ onAccepted?: (files: MediaFile[]) => void;
113
+ onRejected?: (rejections: MediaRejection[]) => void;
114
+ };
115
+
116
+ export type MediaPreviewContext = {
117
+ remove: () => void;
118
+ disabled: boolean;
119
+ readOnly: boolean;
120
+ size: MediaPreviewSize;
121
+ };
122
+
123
+ export type MediaDropzoneContext = {
124
+ isDragging: boolean;
125
+ open: () => void;
126
+ disabled: boolean;
127
+ readOnly: boolean;
128
+ hasFiles: boolean;
129
+ };