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.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/api.d.ts +175 -0
- package/dist/api.js +1954 -0
- package/dist/api.js.map +1 -0
- package/dist/frameworks/astro/astro.config.snippet.md +23 -0
- package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
- package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
- package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
- package/dist/frameworks/next/layout-import.snippet.md +19 -0
- package/dist/frameworks/next/next.config.snippet.md +15 -0
- package/dist/frameworks/next/postcss.config.js.template +16 -0
- package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
- package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
- package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
- package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
- package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
- package/dist/index.js +2047 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/registry/components/primitives/icon/icon.module.css +63 -0
- package/registry/components/primitives/icon/icon.tsx +53 -0
- package/registry/components/primitives/modal/modal-context.ts +19 -0
- package/registry/components/primitives/modal/modal.module.css +415 -0
- package/registry/components/primitives/modal/modal.tsx +550 -0
- package/registry/components/primitives/popover/popover-context.ts +28 -0
- package/registry/components/primitives/popover/popover.module.css +28 -0
- package/registry/components/primitives/popover/popover.tsx +465 -0
- package/registry/components/ui/accordion/accordion.module.css +208 -0
- package/registry/components/ui/accordion/accordion.tsx +327 -0
- package/registry/components/ui/avatar/avatar.module.css +32 -0
- package/registry/components/ui/avatar/avatar.tsx +37 -0
- package/registry/components/ui/badge/badge.module.css +103 -0
- package/registry/components/ui/badge/badge.tsx +29 -0
- package/registry/components/ui/button/button.module.css +216 -0
- package/registry/components/ui/button/button.tsx +133 -0
- package/registry/components/ui/chart/chart.module.css +168 -0
- package/registry/components/ui/chart/chart.tsx +1293 -0
- package/registry/components/ui/chart/types.ts +210 -0
- package/registry/components/ui/chart/utils.ts +59 -0
- package/registry/components/ui/copy-button/copy-button.module.css +21 -0
- package/registry/components/ui/copy-button/copy-button.tsx +123 -0
- package/registry/components/ui/countdown/countdown.module.css +5 -0
- package/registry/components/ui/countdown/countdown.tsx +152 -0
- package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
- package/registry/components/ui/data-table/data-table.module.css +594 -0
- package/registry/components/ui/data-table/data-table.tsx +1109 -0
- package/registry/components/ui/data-table/filter-bar.tsx +33 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
- package/registry/components/ui/data-table/types.ts +594 -0
- package/registry/components/ui/data-table/useDataTable.tsx +573 -0
- package/registry/components/ui/data-table/utils.ts +445 -0
- package/registry/components/ui/empty-state/empty-state.module.css +26 -0
- package/registry/components/ui/empty-state/empty-state.tsx +32 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
- package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
- package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
- package/registry/components/ui/input-fields/common/types.ts +38 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
- package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
- package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
- package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
- package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
- package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
- package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
- package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
- package/registry/components/ui/input-fields/input/input.tsx +319 -0
- package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
- package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
- package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
- package/registry/components/ui/input-fields/media-input/types.ts +129 -0
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
- package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
- package/registry/components/ui/input-fields/select/select.tsx +118 -0
- package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
- package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
- package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
- package/registry/components/ui/loader/loader.module.css +156 -0
- package/registry/components/ui/loader/loader.tsx +102 -0
- package/registry/components/ui/stepper/stepper.module.css +294 -0
- package/registry/components/ui/stepper/stepper.tsx +197 -0
- package/registry/components/ui/tabs/tabs.module.css +150 -0
- package/registry/components/ui/tabs/tabs.tsx +134 -0
- package/registry/components/ui/toast/toast.module.css +159 -0
- package/registry/components/ui/toast/toast.tsx +104 -0
- package/registry/components/ui/tooltip/tooltip.module.css +82 -0
- package/registry/components/ui/tooltip/tooltip.tsx +72 -0
- package/registry/global.d.ts +4 -0
- package/registry/hooks/useFocusOutside.tsx +33 -0
- package/registry/hooks/useResizeObserver.tsx +32 -0
- package/registry/lib/dom.ts +25 -0
- package/registry/lib/string.ts +23 -0
- package/registry/registry.json +719 -0
- package/registry/styles/app.css +240 -0
- package/registry/styles/components.css +78 -0
- package/registry/styles/fonts/geist-regular.woff2 +0 -0
- package/registry/styles/globals.css +7 -0
- package/registry/styles/media.css +25 -0
- package/registry/styles/utilities.css +90 -0
- package/registry/themes/colors/blue.css +37 -0
- package/registry/themes/colors/green.css +37 -0
- package/registry/themes/colors/rose.css +37 -0
- package/registry/themes/colors/slate.css +37 -0
- package/registry/themes/colors/stone.css +37 -0
- package/registry/themes/colors/violet.css +37 -0
- package/registry/themes/colors/zinc.css +37 -0
- package/registry/themes/radius/pill.css +8 -0
- package/registry/themes/radius/rounded.css +8 -0
- package/registry/themes/radius/sharp.css +8 -0
- package/registry/themes/spacing/compact.css +17 -0
- package/registry/themes/spacing/spacious.css +17 -0
- package/registry/tsconfig.json +25 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentProps,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import clsx from "clsx";
|
|
10
|
+
import {
|
|
11
|
+
IconKey,
|
|
12
|
+
IconKeyOff,
|
|
13
|
+
IconSearch,
|
|
14
|
+
IconX,
|
|
15
|
+
IconCalendar,
|
|
16
|
+
IconClock,
|
|
17
|
+
} from "@tabler/icons-react";
|
|
18
|
+
import sCommon from "../common/input-field-common.module.css";
|
|
19
|
+
import sButton from "../../button/button.module.css";
|
|
20
|
+
import s from "../common/input-field-native.module.css";
|
|
21
|
+
import { FieldMessage } from "../common/field-message/field-message";
|
|
22
|
+
import { Icon } from "../../../primitives/icon/icon";
|
|
23
|
+
import type { BaseInputFieldProps } from "../common/types";
|
|
24
|
+
|
|
25
|
+
export type InputType =
|
|
26
|
+
| "text"
|
|
27
|
+
| "password"
|
|
28
|
+
| "search"
|
|
29
|
+
| "number"
|
|
30
|
+
| "radio"
|
|
31
|
+
| "date"
|
|
32
|
+
| "month"
|
|
33
|
+
| "week"
|
|
34
|
+
| "time"
|
|
35
|
+
| "datetime-local"
|
|
36
|
+
| "checkbox"
|
|
37
|
+
| "color"
|
|
38
|
+
| "range";
|
|
39
|
+
|
|
40
|
+
type BaseProps = BaseInputFieldProps & {
|
|
41
|
+
type?: InputType;
|
|
42
|
+
onClear?: () => void;
|
|
43
|
+
showClear?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type InputProps = Omit<ComponentProps<"input">, "type"> & BaseProps;
|
|
47
|
+
|
|
48
|
+
export const Input = ({
|
|
49
|
+
type = "text",
|
|
50
|
+
label,
|
|
51
|
+
errors,
|
|
52
|
+
info,
|
|
53
|
+
className,
|
|
54
|
+
style,
|
|
55
|
+
value,
|
|
56
|
+
layout = "default",
|
|
57
|
+
layoutDirection = "default",
|
|
58
|
+
ref,
|
|
59
|
+
onClear,
|
|
60
|
+
showClear = false,
|
|
61
|
+
onChange,
|
|
62
|
+
buttonStyle = "none",
|
|
63
|
+
iconStart,
|
|
64
|
+
iconEnd,
|
|
65
|
+
...props
|
|
66
|
+
}: InputProps) => {
|
|
67
|
+
const [eyeVisible, setEyeVisible] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Derive initial hasValue from controlled value or defaultValue.
|
|
70
|
+
// IMPORTANT: do NOT call setHasValue inside a ref callback - it triggers
|
|
71
|
+
// a re-render which creates a new ref callback, which React treats as a
|
|
72
|
+
// changed ref, repeating the cycle indefinitely (infinite loop).
|
|
73
|
+
const [hasValue, setHasValue] = useState(() => {
|
|
74
|
+
if (value !== undefined && value !== null && value !== "") return true;
|
|
75
|
+
const dv = props.defaultValue;
|
|
76
|
+
if (dv !== undefined && dv !== null && dv !== "") return true;
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Sync hasValue when controlled value changes externally (e.g. form reset)
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (value !== undefined) {
|
|
83
|
+
setHasValue(String(value).length > 0);
|
|
84
|
+
}
|
|
85
|
+
}, [value]);
|
|
86
|
+
|
|
87
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
88
|
+
|
|
89
|
+
const isPassword = type === "password";
|
|
90
|
+
const isSearch = type === "search";
|
|
91
|
+
|
|
92
|
+
const inputType = isPassword && eyeVisible ? "text" : type;
|
|
93
|
+
// useId() guarantees a stable, unique id per Input instance. Falling back
|
|
94
|
+
// to `name` previously broke radio/checkbox siblings (they share a name,
|
|
95
|
+
// so every label's htmlFor pointed to the first input in the DOM).
|
|
96
|
+
const generatedId = useId();
|
|
97
|
+
const inputId = props.id ?? generatedId;
|
|
98
|
+
|
|
99
|
+
const showPasswordToggle = isPassword && hasValue;
|
|
100
|
+
const showClearButton =
|
|
101
|
+
showClear &&
|
|
102
|
+
hasValue &&
|
|
103
|
+
![
|
|
104
|
+
"radio",
|
|
105
|
+
"range",
|
|
106
|
+
"color",
|
|
107
|
+
"date",
|
|
108
|
+
"month",
|
|
109
|
+
"week",
|
|
110
|
+
"time",
|
|
111
|
+
"datetime-local",
|
|
112
|
+
"checkbox",
|
|
113
|
+
].includes(type);
|
|
114
|
+
|
|
115
|
+
const stylesToApplyFields = [
|
|
116
|
+
"text",
|
|
117
|
+
"password",
|
|
118
|
+
"search",
|
|
119
|
+
"number",
|
|
120
|
+
"date",
|
|
121
|
+
"month",
|
|
122
|
+
"week",
|
|
123
|
+
"time",
|
|
124
|
+
"datetime-local",
|
|
125
|
+
"file",
|
|
126
|
+
].includes(type);
|
|
127
|
+
const stylesFitWidth = ["checkbox", "radio", "color"].includes(type);
|
|
128
|
+
const hasButtonStyle = buttonStyle !== "none";
|
|
129
|
+
const buttonToneClass = hasButtonStyle
|
|
130
|
+
? sButton[`tone_${buttonStyle}`]
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
133
|
+
const isDateFamily = ["date", "month", "week", "datetime-local"].includes(
|
|
134
|
+
type,
|
|
135
|
+
);
|
|
136
|
+
const isTimeFamily = type === "time";
|
|
137
|
+
|
|
138
|
+
const showDateCalendarIcon = isDateFamily && !iconEnd;
|
|
139
|
+
const showTimeClockIcon = isTimeFamily && !iconEnd;
|
|
140
|
+
|
|
141
|
+
const startActionCount = iconStart ? 1 : isSearch ? 1 : 0;
|
|
142
|
+
const endActionCount = [
|
|
143
|
+
showClearButton,
|
|
144
|
+
showPasswordToggle,
|
|
145
|
+
showDateCalendarIcon,
|
|
146
|
+
showTimeClockIcon,
|
|
147
|
+
iconEnd,
|
|
148
|
+
].filter(Boolean).length;
|
|
149
|
+
|
|
150
|
+
const dynamicInputStyle = {
|
|
151
|
+
...style,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
155
|
+
setHasValue(e.target.value.length > 0);
|
|
156
|
+
onChange?.(e);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleClear = () => {
|
|
160
|
+
const element = inputRef.current;
|
|
161
|
+
if (!element) return;
|
|
162
|
+
|
|
163
|
+
if (onClear) {
|
|
164
|
+
onClear();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (onChange) {
|
|
169
|
+
element.value = "";
|
|
170
|
+
setHasValue(false);
|
|
171
|
+
const event = {
|
|
172
|
+
target: element,
|
|
173
|
+
currentTarget: element,
|
|
174
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
175
|
+
onChange(event);
|
|
176
|
+
element.focus();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
element.value = "";
|
|
181
|
+
setHasValue(false);
|
|
182
|
+
element.focus();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Stable ref callback - memoized so React doesn't treat it as a new ref
|
|
186
|
+
// on every render. Does NOT call setState (would cause infinite loop).
|
|
187
|
+
const refCallback = useCallback(
|
|
188
|
+
(element: HTMLInputElement | null) => {
|
|
189
|
+
inputRef.current = element;
|
|
190
|
+
if (ref) {
|
|
191
|
+
if (typeof ref === "function") {
|
|
192
|
+
ref(element);
|
|
193
|
+
} else {
|
|
194
|
+
ref.current = element;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[ref],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className={clsx(sCommon.container, className)} style={style}>
|
|
203
|
+
<div
|
|
204
|
+
className={clsx(
|
|
205
|
+
sCommon.field_container,
|
|
206
|
+
layout !== "default" && sCommon[layout],
|
|
207
|
+
layout !== "default" &&
|
|
208
|
+
layoutDirection !== "default" &&
|
|
209
|
+
sCommon[layoutDirection],
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
{label && (
|
|
213
|
+
<label className={sCommon.label} htmlFor={inputId}>
|
|
214
|
+
{label}
|
|
215
|
+
</label>
|
|
216
|
+
)}
|
|
217
|
+
<div
|
|
218
|
+
className={clsx(
|
|
219
|
+
sCommon.field_wrapper,
|
|
220
|
+
s.native_field_wrapper,
|
|
221
|
+
hasButtonStyle && sCommon.field_wrapper_variant,
|
|
222
|
+
hasButtonStyle && buttonToneClass,
|
|
223
|
+
stylesFitWidth && sCommon.field_wrapper_fit,
|
|
224
|
+
)}
|
|
225
|
+
data-start-actions={
|
|
226
|
+
startActionCount > 0 ? startActionCount : undefined
|
|
227
|
+
}
|
|
228
|
+
data-end-actions={
|
|
229
|
+
endActionCount > 0 ? Math.min(3, endActionCount) : undefined
|
|
230
|
+
}
|
|
231
|
+
>
|
|
232
|
+
{iconStart ? (
|
|
233
|
+
<div className={clsx(sCommon.icon_start)}>{iconStart}</div>
|
|
234
|
+
) : isSearch ? (
|
|
235
|
+
<Icon className={clsx(sCommon.icon_start)}>
|
|
236
|
+
<IconSearch />
|
|
237
|
+
</Icon>
|
|
238
|
+
) : null}
|
|
239
|
+
|
|
240
|
+
<input
|
|
241
|
+
id={inputId}
|
|
242
|
+
name={props.name}
|
|
243
|
+
type={inputType}
|
|
244
|
+
value={value}
|
|
245
|
+
ref={refCallback}
|
|
246
|
+
className={clsx(
|
|
247
|
+
stylesToApplyFields &&
|
|
248
|
+
buttonStyle === "none" &&
|
|
249
|
+
sCommon.field_default,
|
|
250
|
+
stylesToApplyFields &&
|
|
251
|
+
hasButtonStyle && [
|
|
252
|
+
sCommon.field_full_width,
|
|
253
|
+
sCommon.field_cursor_text,
|
|
254
|
+
sCommon.field_button_variant,
|
|
255
|
+
buttonToneClass,
|
|
256
|
+
sButton.base,
|
|
257
|
+
sButton[buttonStyle],
|
|
258
|
+
sButton.input_focus,
|
|
259
|
+
],
|
|
260
|
+
)}
|
|
261
|
+
style={dynamicInputStyle}
|
|
262
|
+
{...props}
|
|
263
|
+
onChange={handleInputChange}
|
|
264
|
+
/>
|
|
265
|
+
|
|
266
|
+
<div className={clsx(sCommon.action_end_wrapper)}>
|
|
267
|
+
{showClearButton && (
|
|
268
|
+
<Icon onClick={handleClear} aria-label="Clear input">
|
|
269
|
+
<IconX />
|
|
270
|
+
</Icon>
|
|
271
|
+
)}
|
|
272
|
+
{showPasswordToggle && (
|
|
273
|
+
<Icon
|
|
274
|
+
onClick={(e) => {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
setEyeVisible((v) => !v);
|
|
277
|
+
}}
|
|
278
|
+
aria-label={eyeVisible ? "Hide password" : "Show password"}
|
|
279
|
+
>
|
|
280
|
+
{eyeVisible ? <IconKeyOff /> : <IconKey />}
|
|
281
|
+
</Icon>
|
|
282
|
+
)}
|
|
283
|
+
{showDateCalendarIcon && (
|
|
284
|
+
<Icon
|
|
285
|
+
onClick={() => {
|
|
286
|
+
try {
|
|
287
|
+
inputRef.current?.showPicker();
|
|
288
|
+
} catch {
|
|
289
|
+
// showPicker is not supported in all browsers.
|
|
290
|
+
}
|
|
291
|
+
}}
|
|
292
|
+
clickable
|
|
293
|
+
>
|
|
294
|
+
<IconCalendar />
|
|
295
|
+
</Icon>
|
|
296
|
+
)}
|
|
297
|
+
{showTimeClockIcon && (
|
|
298
|
+
<Icon
|
|
299
|
+
onClick={() => {
|
|
300
|
+
try {
|
|
301
|
+
inputRef.current?.showPicker();
|
|
302
|
+
} catch {
|
|
303
|
+
// showPicker is not supported in all browsers.
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
306
|
+
clickable
|
|
307
|
+
>
|
|
308
|
+
<IconClock />
|
|
309
|
+
</Icon>
|
|
310
|
+
)}
|
|
311
|
+
{iconEnd}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<FieldMessage errors={errors} info={info} />
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* Helpers-only file (no React components) - keeps fast-refresh happy by
|
|
2
|
+
isolating JSX-returning helpers from component-bearing modules. */
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
IconCode,
|
|
6
|
+
IconFile,
|
|
7
|
+
IconFileMusic,
|
|
8
|
+
IconFileText,
|
|
9
|
+
IconFileTypePdf,
|
|
10
|
+
IconFileTypePpt,
|
|
11
|
+
IconFileTypeXls,
|
|
12
|
+
IconFileTypeDoc,
|
|
13
|
+
IconFileTypography,
|
|
14
|
+
IconFileZip,
|
|
15
|
+
IconPhoto,
|
|
16
|
+
IconVideo,
|
|
17
|
+
} from "@tabler/icons-react";
|
|
18
|
+
import type { MediaKind } from "./types";
|
|
19
|
+
|
|
20
|
+
export const KIND_LABEL: Record<MediaKind, string> = {
|
|
21
|
+
image: "Image",
|
|
22
|
+
video: "Video",
|
|
23
|
+
audio: "Audio",
|
|
24
|
+
pdf: "PDF",
|
|
25
|
+
document: "Document",
|
|
26
|
+
spreadsheet: "Spreadsheet",
|
|
27
|
+
presentation: "Presentation",
|
|
28
|
+
archive: "Archive",
|
|
29
|
+
code: "Code",
|
|
30
|
+
text: "Text",
|
|
31
|
+
font: "Font",
|
|
32
|
+
other: "File",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const iconForKind = (kind: MediaKind): ReactNode => {
|
|
36
|
+
switch (kind) {
|
|
37
|
+
case "image":
|
|
38
|
+
return <IconPhoto />;
|
|
39
|
+
case "video":
|
|
40
|
+
return <IconVideo />;
|
|
41
|
+
case "audio":
|
|
42
|
+
return <IconFileMusic />;
|
|
43
|
+
case "pdf":
|
|
44
|
+
return <IconFileTypePdf />;
|
|
45
|
+
case "document":
|
|
46
|
+
return <IconFileTypeDoc />;
|
|
47
|
+
case "spreadsheet":
|
|
48
|
+
return <IconFileTypeXls />;
|
|
49
|
+
case "presentation":
|
|
50
|
+
return <IconFileTypePpt />;
|
|
51
|
+
case "archive":
|
|
52
|
+
return <IconFileZip />;
|
|
53
|
+
case "code":
|
|
54
|
+
return <IconCode />;
|
|
55
|
+
case "text":
|
|
56
|
+
return <IconFileText />;
|
|
57
|
+
case "font":
|
|
58
|
+
return <IconFileTypography />;
|
|
59
|
+
case "other":
|
|
60
|
+
return <IconFile />;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/* Media Input - composes the shared input-field shell.
|
|
2
|
+
Outer container, label row, and field-message slot are reused from
|
|
3
|
+
`input-field-common.module.css` so the family layout stays identical.
|
|
4
|
+
This module owns the dropzone target, clear-all action styling, and the
|
|
5
|
+
preview list/tiles. */
|
|
6
|
+
|
|
7
|
+
.clear_all {
|
|
8
|
+
--button-fg: var(--color-muted-foreground);
|
|
9
|
+
gap: var(--spacing-1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ── Dropzone ─────────────────────────────────────────────────────────
|
|
13
|
+
Mirrors the SelectButton split from common/select-button:
|
|
14
|
+
- Outer `.dropzone_wrapper` carries the variant CSS-variable contracts
|
|
15
|
+
(paired with `field_wrapper_variant` + `tone_<variant>` from common +
|
|
16
|
+
button modules).
|
|
17
|
+
- Inner element is one of:
|
|
18
|
+
- `.dropzone .dropzone_default` (when buttonStyle === "none"):
|
|
19
|
+
dashed muted drop target, fully painted by this module.
|
|
20
|
+
- `.dropzone .dropzone_button_shell` rendered as
|
|
21
|
+
`<Button as="div" variant=X>`: paint comes from Button (.base + .X
|
|
22
|
+
+ tone), this module only enforces dropzone sizing and the drag
|
|
23
|
+
focus-ring on top of that paint.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
.dropzone_wrapper {
|
|
27
|
+
width: 100%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Base shape - no paint. Both "none" and buttonStyle modes share these. */
|
|
31
|
+
.dropzone {
|
|
32
|
+
width: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
outline: none;
|
|
38
|
+
transition:
|
|
39
|
+
border-color var(--transition-base),
|
|
40
|
+
background-color var(--transition-base),
|
|
41
|
+
box-shadow var(--transition-base);
|
|
42
|
+
|
|
43
|
+
&.dropzone_disabled {
|
|
44
|
+
cursor: not-allowed;
|
|
45
|
+
opacity: 0.6;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Default look - applied when buttonStyle === "none". Owns padding,
|
|
51
|
+
min-height, border-radius and the dashed muted paint. */
|
|
52
|
+
.dropzone_default {
|
|
53
|
+
min-height: var(--spacing-13);
|
|
54
|
+
padding: var(--spacing-7) var(--spacing-6);
|
|
55
|
+
border: var(--border-1) dashed var(--color-border);
|
|
56
|
+
border-radius: var(--radius-md);
|
|
57
|
+
background-color: var(--color-muted);
|
|
58
|
+
|
|
59
|
+
&:hover:not(.dropzone_disabled) {
|
|
60
|
+
border-color: var(--color-ring);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&:focus-visible {
|
|
64
|
+
border-color: var(--color-ring);
|
|
65
|
+
box-shadow: var(--shadow-ring);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&.dropzone_active {
|
|
69
|
+
border-color: var(--color-ring);
|
|
70
|
+
background-color: var(--color-background);
|
|
71
|
+
box-shadow: var(--shadow-ring);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Override Button's compact width / min-height / padding / border-radius
|
|
76
|
+
when the dropzone is rendered as `<Button as="div" variant=X>`. Same
|
|
77
|
+
approach as `field_full_width` in input-field-common - targeted micro-
|
|
78
|
+
class with `!important` to defeat Button's `.base` defaults regardless
|
|
79
|
+
of stylesheet load order. */
|
|
80
|
+
.dropzone_button_shell {
|
|
81
|
+
min-height: var(--spacing-13) !important;
|
|
82
|
+
padding: var(--spacing-7) var(--spacing-6) !important;
|
|
83
|
+
border-radius: var(--radius-md) !important;
|
|
84
|
+
justify-content: center !important;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Drag-active feedback for the buttonStyle branch - focus ring layered
|
|
88
|
+
on top of the variant's own paint. */
|
|
89
|
+
.dropzone_button_shell.dropzone_active {
|
|
90
|
+
box-shadow: var(--shadow-ring) !important;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.dropzone_inner {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
text-align: center;
|
|
99
|
+
gap: var(--spacing-5);
|
|
100
|
+
width: 100%;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dropzone_icon {
|
|
104
|
+
--icon-color: var(--field-icon-color, var(--color-muted-foreground));
|
|
105
|
+
color: var(--field-icon-color, var(--color-muted-foreground));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dropzone_copy {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
gap: var(--spacing-2);
|
|
112
|
+
align-items: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dropzone_title {
|
|
116
|
+
font-family: var(--font-family-1);
|
|
117
|
+
font-size: var(--font-size-sm);
|
|
118
|
+
color: var(--field-content-color, var(--color-foreground));
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
line-height: 1.3;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dropzone_hint {
|
|
124
|
+
font-family: var(--font-family-1);
|
|
125
|
+
font-size: var(--font-size-xs);
|
|
126
|
+
color: var(--field-placeholder-color, var(--color-muted-foreground));
|
|
127
|
+
line-height: 1.4;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ── Preview list ─────────────────────────────────────────────────────── */
|
|
131
|
+
|
|
132
|
+
/* Track width and gap are owned by the list (they're layout concerns).
|
|
133
|
+
Thumb height is owned by the tile via `.preview_size_*` so a standalone
|
|
134
|
+
`<MediaPreview size="sm">` works without needing a parent list to scope
|
|
135
|
+
the CSS variable. Track is fixed (not 1fr) so size differences are
|
|
136
|
+
visible - `min(track, 100%)` keeps it responsive on narrow containers. */
|
|
137
|
+
.preview_list {
|
|
138
|
+
list-style: none;
|
|
139
|
+
margin: 0;
|
|
140
|
+
padding: 0;
|
|
141
|
+
display: grid;
|
|
142
|
+
gap: var(--spacing-4);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.preview_list_sm {
|
|
146
|
+
grid-template-columns: repeat(auto-fill, min(7rem, 100%));
|
|
147
|
+
gap: var(--spacing-3);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.preview_list_md {
|
|
151
|
+
grid-template-columns: repeat(auto-fill, min(10rem, 100%));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.preview_list_lg {
|
|
155
|
+
grid-template-columns: repeat(auto-fill, min(14rem, 100%));
|
|
156
|
+
gap: var(--spacing-5);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.preview_list_layout_list {
|
|
160
|
+
grid-template-columns: 1fr;
|
|
161
|
+
gap: var(--spacing-3);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.preview_size_sm {
|
|
165
|
+
--preview-thumb-h: 4.5rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.preview_size_md {
|
|
169
|
+
--preview-thumb-h: 6.5rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.preview_size_lg {
|
|
173
|
+
--preview-thumb-h: 9.5rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.preview_item_wrap {
|
|
177
|
+
display: flex;
|
|
178
|
+
min-width: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.preview_item {
|
|
182
|
+
position: relative;
|
|
183
|
+
width: 100%;
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
gap: var(--spacing-3);
|
|
187
|
+
padding: var(--spacing-3);
|
|
188
|
+
border: var(--border-1) solid var(--color-border);
|
|
189
|
+
border-radius: var(--radius-md);
|
|
190
|
+
background-color: var(--color-background);
|
|
191
|
+
transition: border-color var(--transition-fast);
|
|
192
|
+
|
|
193
|
+
&:hover {
|
|
194
|
+
border-color: var(--color-ring);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&[data-status="error"] {
|
|
198
|
+
border-color: var(--color-error-foreground);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* List layout - single row with thumb on the left and metadata fills the rest.
|
|
203
|
+
`flex: 1` on .preview_meta naturally pushes the remove button to the end -
|
|
204
|
+
no margin tricks needed. */
|
|
205
|
+
.preview_item_list {
|
|
206
|
+
flex-direction: row;
|
|
207
|
+
align-items: center;
|
|
208
|
+
padding: var(--spacing-3) var(--spacing-4);
|
|
209
|
+
gap: var(--spacing-4);
|
|
210
|
+
|
|
211
|
+
.preview_thumb {
|
|
212
|
+
width: var(--spacing-12);
|
|
213
|
+
height: var(--spacing-12);
|
|
214
|
+
flex-shrink: 0;
|
|
215
|
+
aspect-ratio: 1 / 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.preview_meta {
|
|
219
|
+
flex: 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.preview_remove {
|
|
223
|
+
position: static;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.preview_thumb {
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: var(--preview-thumb-h, 6.5rem);
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
border-radius: var(--radius-sm);
|
|
235
|
+
background-color: var(--color-muted);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.preview_image {
|
|
239
|
+
width: 100%;
|
|
240
|
+
height: 100%;
|
|
241
|
+
object-fit: cover;
|
|
242
|
+
display: block;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.preview_thumb_fallback {
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
justify-content: center;
|
|
249
|
+
width: 100%;
|
|
250
|
+
height: 100%;
|
|
251
|
+
--icon-color: var(--color-muted-foreground);
|
|
252
|
+
color: var(--color-muted-foreground);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.preview_meta {
|
|
256
|
+
min-width: 0;
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: var(--spacing-1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.preview_name {
|
|
263
|
+
font-family: var(--font-family-1);
|
|
264
|
+
font-size: var(--font-size-xs);
|
|
265
|
+
color: var(--color-foreground);
|
|
266
|
+
font-weight: 500;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
line-height: 1.3;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.preview_sub {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: var(--spacing-1);
|
|
277
|
+
font-family: var(--font-family-1);
|
|
278
|
+
font-size: var(--font-size-xs);
|
|
279
|
+
color: var(--color-muted-foreground);
|
|
280
|
+
line-height: 1.3;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.preview_error {
|
|
284
|
+
font-family: var(--font-family-1);
|
|
285
|
+
font-size: var(--font-size-xs);
|
|
286
|
+
color: var(--color-error-foreground);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Position only - visuals come from the Button component (variant + size + box layout). */
|
|
290
|
+
.preview_remove {
|
|
291
|
+
position: absolute;
|
|
292
|
+
top: var(--spacing-2);
|
|
293
|
+
right: var(--spacing-2);
|
|
294
|
+
}
|