aplance-ui 1.2.1 → 1.2.2

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.
@@ -22,24 +22,67 @@ import { FieldMessage } from "../common/field-message/field-message";
22
22
  import { Icon } from "../../../primitives/icon/icon";
23
23
  import type { BaseInputFieldProps } from "../common/types";
24
24
 
25
- export type InputType =
26
- | "text"
27
- | "password"
28
- | "search"
29
- | "number"
30
- | "radio"
31
- | "date"
32
- | "month"
25
+ export type InputType =
26
+ | "text"
27
+ | "email"
28
+ | "tel"
29
+ | "url"
30
+ | "password"
31
+ | "search"
32
+ | "number"
33
+ | "file"
34
+ | "radio"
35
+ | "date"
36
+ | "month"
33
37
  | "week"
34
38
  | "time"
35
39
  | "datetime-local"
36
40
  | "checkbox"
37
- | "color"
38
- | "range";
39
-
40
- type BaseProps = BaseInputFieldProps & {
41
- type?: InputType;
42
- onClear?: () => void;
41
+ | "color"
42
+ | "range";
43
+
44
+ const styledInputTypes: readonly InputType[] = [
45
+ "text",
46
+ "email",
47
+ "tel",
48
+ "url",
49
+ "password",
50
+ "search",
51
+ "number",
52
+ "file",
53
+ "date",
54
+ "month",
55
+ "week",
56
+ "time",
57
+ "datetime-local",
58
+ ] as const;
59
+
60
+ const clearableInputTypes: readonly InputType[] = [
61
+ "text",
62
+ "email",
63
+ "tel",
64
+ "url",
65
+ "password",
66
+ "search",
67
+ "number",
68
+ "file",
69
+ ] as const;
70
+
71
+ const fitWidthInputTypes: readonly InputType[] = [
72
+ "checkbox",
73
+ "radio",
74
+ "color",
75
+ ] as const;
76
+ const dateInputTypes: readonly InputType[] = [
77
+ "date",
78
+ "month",
79
+ "week",
80
+ "datetime-local",
81
+ ] as const;
82
+
83
+ type BaseProps = BaseInputFieldProps & {
84
+ type?: InputType;
85
+ onClear?: () => void;
43
86
  showClear?: boolean;
44
87
  };
45
88
 
@@ -96,43 +139,18 @@ export const Input = ({
96
139
  const generatedId = useId();
97
140
  const inputId = props.id ?? generatedId;
98
141
 
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
- );
142
+ const showPasswordToggle = isPassword && hasValue;
143
+ const showClearButton =
144
+ showClear && hasValue && clearableInputTypes.includes(type);
145
+
146
+ const stylesToApplyFields = styledInputTypes.includes(type);
147
+ const stylesFitWidth = fitWidthInputTypes.includes(type);
148
+ const hasButtonStyle = buttonStyle !== "none";
149
+ const buttonToneClass = hasButtonStyle
150
+ ? sButton[`tone_${buttonStyle}`]
151
+ : undefined;
152
+
153
+ const isDateFamily = dateInputTypes.includes(type);
136
154
  const isTimeFamily = type === "time";
137
155
 
138
156
  const showDateCalendarIcon = isDateFamily && !iconEnd;
@@ -204,6 +204,35 @@
204
204
  &[data-status="error"] {
205
205
  border-color: var(--color-error-foreground);
206
206
  }
207
+
208
+ &[data-status="uploaded"] {
209
+ border-color: var(--color-success-foreground);
210
+ }
211
+ }
212
+
213
+ /* Progress bar shown along the bottom edge of a tile during upload. The
214
+ width comes from `style={{ width: <pct>% }}` set inline on .progress_fill. */
215
+ .preview_progress_track {
216
+ width: 100%;
217
+ height: var(--spacing-2);
218
+ border-radius: var(--radius-full);
219
+ background-color: var(--color-muted);
220
+ overflow: hidden;
221
+ }
222
+
223
+ .preview_progress_fill {
224
+ height: 100%;
225
+ border-radius: var(--radius-full);
226
+ background-color: var(--color-primary);
227
+ transition: width var(--transition-base);
228
+ }
229
+
230
+ .preview_done_badge {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ gap: var(--spacing-2);
234
+ color: var(--color-success-foreground);
235
+ font-size: var(--font-size-xs);
207
236
  }
208
237
 
209
238
  /* List layout - single row with thumb on the left and metadata fills the rest.
@@ -1,13 +1,9 @@
1
1
  import { type FC, type MouseEvent } from "react";
2
2
  import clsx from "clsx";
3
- import { IconX } from "@tabler/icons-react";
3
+ import { IconCircleCheck, IconX } from "@tabler/icons-react";
4
4
  import { Icon } from "../../../primitives/icon/icon";
5
5
  import { Button } from "../../button/button";
6
- import type {
7
- MediaFile,
8
- MediaPreviewLayout,
9
- MediaPreviewSize,
10
- } from "./types";
6
+ import type { MediaFile, MediaPreviewLayout, MediaPreviewSize } from "./types";
11
7
  import { formatBytes, previewKindOf } from "./utils";
12
8
  import { KIND_LABEL, iconForKind } from "./kind-icons";
13
9
  import s from "./media-input.module.css";
@@ -72,9 +68,31 @@ export const MediaPreview: FC<MediaPreviewProps> = ({
72
68
  </span>
73
69
  <span className={s.preview_sub}>
74
70
  <span>{KIND_LABEL[kind]}</span>
75
- <span aria-hidden="true">·</span>
71
+ <span aria-hidden="true">-</span>
76
72
  <span>{formatBytes(file.file.size)}</span>
77
73
  </span>
74
+ {file.status === "uploading" && typeof file.progress === "number" ? (
75
+ <div
76
+ className={s.preview_progress_track}
77
+ role="progressbar"
78
+ aria-valuemin={0}
79
+ aria-valuemax={100}
80
+ aria-valuenow={file.progress}
81
+ >
82
+ <div
83
+ className={s.preview_progress_fill}
84
+ style={{ width: `${String(file.progress)}%` }}
85
+ />
86
+ </div>
87
+ ) : null}
88
+ {file.status === "uploaded" ? (
89
+ <span className={s.preview_done_badge}>
90
+ <Icon size="sm" aria-hidden="true">
91
+ <IconCircleCheck />
92
+ </Icon>
93
+ Uploaded
94
+ </span>
95
+ ) : null}
78
96
  {file.error && <span className={s.preview_error}>{file.error}</span>}
79
97
  </div>
80
98
 
@@ -3,14 +3,23 @@ import { type RefObject, useEffect } from "react";
3
3
  type UseFocusOutsideOptions = {
4
4
  enabled?: boolean;
5
5
  event?: "mousedown" | "mouseup" | "click";
6
+ /**
7
+ * CSS selector for elements that count as "inside" even when they aren't
8
+ * contained by any `ref`. Needed for portaled descendant layers - a
9
+ * `Popover` menu opened from within a `Modal` renders under `document.body`
10
+ * via `createPortal`, so it's outside the modal's `contentRef` even though
11
+ * it's logically nested. Without this, the modal's outside-click handler
12
+ * fires when you click a dropdown option and dismisses the dialog.
13
+ */
14
+ ignoreSelector?: string;
6
15
  };
7
16
 
8
17
  export function useFocusOutside(
9
18
  refs: RefObject<HTMLElement | null>[],
10
19
  handler: (event: MouseEvent) => void,
11
- options: UseFocusOutsideOptions = {}
20
+ options: UseFocusOutsideOptions = {},
12
21
  ) {
13
- const { enabled = true, event = "mousedown" } = options;
22
+ const { enabled = true, event = "mousedown", ignoreSelector } = options;
14
23
 
15
24
  useEffect(() => {
16
25
  if (!enabled) return;
@@ -19,15 +28,21 @@ export function useFocusOutside(
19
28
  const target = e.target as Node;
20
29
 
21
30
  const clickedInside = refs.some(
22
- (ref) => ref.current && ref.current.contains(target)
31
+ (ref) => ref.current && ref.current.contains(target),
23
32
  );
33
+ if (clickedInside) return;
24
34
 
25
- if (!clickedInside) {
26
- handler(e);
35
+ // A click inside a portaled layer (e.g. a popover menu) counts as
36
+ // "inside" so the host (modal/drawer) doesn't dismiss itself.
37
+ if (ignoreSelector) {
38
+ const el = target instanceof Element ? target : target.parentElement;
39
+ if (el?.closest(ignoreSelector)) return;
27
40
  }
41
+
42
+ handler(e);
28
43
  };
29
44
 
30
45
  document.addEventListener(event, listener);
31
46
  return () => document.removeEventListener(event, listener);
32
- }, [refs, handler, enabled, event]);
47
+ }, [refs, handler, enabled, event, ignoreSelector]);
33
48
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://ui.aplance.com/registry.schema.json",
3
3
  "version": 4,
4
- "generated": "2026-05-15T13:59:14.737Z",
4
+ "generated": "2026-05-26T16:15:51.026Z",
5
5
  "units": [
6
6
  {
7
7
  "name": "accordion",
@@ -199,6 +199,7 @@
199
199
  "select-button"
200
200
  ],
201
201
  "externalDeps": [
202
+ "@tabler/icons-react",
202
203
  "clsx",
203
204
  "react"
204
205
  ]