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,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
+ }