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.
- package/package.json +1 -1
- package/registry/components/primitives/modal/modal.tsx +46 -13
- package/registry/components/primitives/popover/popover.tsx +6 -0
- package/registry/components/ui/data-table/data-table.module.css +89 -20
- package/registry/components/ui/data-table/data-table.tsx +193 -111
- package/registry/components/ui/data-table/pagination-bar.tsx +174 -71
- package/registry/components/ui/data-table/types.ts +12 -3
- package/registry/components/ui/input-fields/common/input-field-native.module.css +52 -42
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +76 -10
- package/registry/components/ui/input-fields/input/input.tsx +69 -51
- package/registry/components/ui/input-fields/media-input/media-input.module.css +29 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +25 -7
- package/registry/hooks/useFocusOutside.tsx +21 -6
- package/registry/registry.json +2 -1
|
@@ -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
|
-
| "
|
|
28
|
-
| "
|
|
29
|
-
| "
|
|
30
|
-
| "
|
|
31
|
-
| "
|
|
32
|
-
| "
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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"
|
|
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
|
-
|
|
26
|
-
|
|
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
|
}
|
package/registry/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://ui.aplance.com/registry.schema.json",
|
|
3
3
|
"version": 4,
|
|
4
|
-
"generated": "2026-05-
|
|
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
|
]
|