@usefui/components 1.6.0 → 1.7.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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +380 -52
- package/dist/index.d.ts +380 -52
- package/dist/index.js +2532 -511
- package/dist/index.mjs +2518 -508
- package/package.json +3 -3
- package/src/__tests__/Avatar.test.tsx +55 -55
- package/src/accordion/Accordion.stories.tsx +6 -4
- package/src/accordion/index.tsx +1 -2
- package/src/avatar/Avatar.stories.tsx +37 -7
- package/src/avatar/index.tsx +90 -19
- package/src/avatar/styles/index.ts +58 -12
- package/src/badge/Badge.stories.tsx +27 -5
- package/src/badge/index.tsx +21 -13
- package/src/badge/styles/index.ts +69 -40
- package/src/button/Button.stories.tsx +40 -27
- package/src/button/index.tsx +13 -9
- package/src/button/styles/index.ts +308 -47
- package/src/card/index.tsx +2 -4
- package/src/checkbox/Checkbox.stories.tsx +72 -33
- package/src/checkbox/index.tsx +8 -6
- package/src/checkbox/styles/index.ts +239 -19
- package/src/collapsible/Collapsible.stories.tsx +6 -4
- package/src/dialog/Dialog.stories.tsx +173 -31
- package/src/dialog/styles/index.ts +13 -8
- package/src/dropdown/Dropdown.stories.tsx +61 -23
- package/src/dropdown/index.tsx +42 -31
- package/src/dropdown/styles/index.ts +30 -19
- package/src/field/Field.stories.tsx +183 -24
- package/src/field/index.tsx +930 -13
- package/src/field/styles/index.ts +246 -14
- package/src/field/types/index.ts +31 -0
- package/src/field/utils/index.ts +201 -0
- package/src/index.ts +2 -1
- package/src/message-bubble/MessageBubble.stories.tsx +59 -12
- package/src/message-bubble/index.tsx +22 -4
- package/src/message-bubble/styles/index.ts +4 -7
- package/src/otp-field/OTPField.stories.tsx +22 -24
- package/src/otp-field/index.tsx +9 -0
- package/src/otp-field/styles/index.ts +114 -16
- package/src/otp-field/types/index.ts +9 -1
- package/src/overlay/styles/index.ts +1 -0
- package/src/ruler/Ruler.stories.tsx +43 -0
- package/src/ruler/constants/index.ts +3 -0
- package/src/ruler/hooks/index.tsx +53 -0
- package/src/ruler/index.tsx +239 -0
- package/src/ruler/styles/index.tsx +154 -0
- package/src/ruler/types/index.ts +17 -0
- package/src/select/Select.stories.tsx +91 -0
- package/src/select/hooks/index.tsx +71 -0
- package/src/select/index.tsx +331 -0
- package/src/select/styles/index.tsx +156 -0
- package/src/shimmer/Shimmer.stories.tsx +6 -4
- package/src/skeleton/index.tsx +7 -6
- package/src/spinner/Spinner.stories.tsx +29 -4
- package/src/spinner/index.tsx +16 -6
- package/src/spinner/styles/index.ts +41 -22
- package/src/switch/Switch.stories.tsx +46 -17
- package/src/switch/index.tsx +5 -8
- package/src/switch/styles/index.ts +45 -45
- package/src/tabs/Tabs.stories.tsx +43 -15
- package/src/text-area/Textarea.stories.tsx +45 -8
- package/src/text-area/index.tsx +9 -6
- package/src/text-area/styles/index.ts +1 -1
- package/src/toggle/Toggle.stories.tsx +6 -4
- package/src/tree/Tree.stories.tsx +6 -4
- package/src/privacy-field/PrivacyField.stories.tsx +0 -29
- package/src/privacy-field/index.tsx +0 -56
- package/src/privacy-field/styles/index.ts +0 -17
|
@@ -10,15 +10,17 @@ export const FieldDefaultStyles = css`
|
|
|
10
10
|
|
|
11
11
|
font-size: var(--fontsize-medium-20);
|
|
12
12
|
|
|
13
|
-
line-height: 1
|
|
13
|
+
line-height: 1;
|
|
14
14
|
letter-spacing: calc(
|
|
15
15
|
var(--fontsize-small-10) - ((var(--fontsize-small-10) * 1.066))
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
border: var(--measurement-small-10) solid transparent;
|
|
19
|
+
|
|
19
20
|
backdrop-filter: blur(var(--measurement-small-10));
|
|
20
21
|
color: var(--font-color-alpha-60);
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
width: 100%;
|
|
22
24
|
height: fit-content;
|
|
23
25
|
|
|
24
26
|
transition: all ease-in-out 0.2s;
|
|
@@ -31,7 +33,9 @@ export const FieldDefaultStyles = css`
|
|
|
31
33
|
|
|
32
34
|
&:hover,
|
|
33
35
|
&:focus,
|
|
34
|
-
&:active
|
|
36
|
+
&:active,
|
|
37
|
+
&:focus-within,
|
|
38
|
+
&:has(:active) {
|
|
35
39
|
color: var(--font-color);
|
|
36
40
|
svg,
|
|
37
41
|
span,
|
|
@@ -44,26 +48,46 @@ export const FieldDefaultStyles = css`
|
|
|
44
48
|
color: var(--font-color-alpha-30);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
&:disabled
|
|
51
|
+
&:disabled,
|
|
52
|
+
&:has(:disabled) {
|
|
48
53
|
cursor: not-allowed;
|
|
49
54
|
opacity: 0.6;
|
|
50
55
|
}
|
|
51
56
|
`;
|
|
52
57
|
export const FieldVariantsStyles = css`
|
|
53
58
|
&[data-variant="primary"] {
|
|
54
|
-
background-color:
|
|
59
|
+
background-color: transparent;
|
|
55
60
|
border-color: var(--font-color-alpha-10);
|
|
56
61
|
|
|
62
|
+
&:hover,
|
|
57
63
|
&:focus,
|
|
58
|
-
&:active
|
|
59
|
-
|
|
64
|
+
&:active,
|
|
65
|
+
&:focus-within,
|
|
66
|
+
&:has(:hover),
|
|
67
|
+
&:has(:active) {
|
|
68
|
+
border-color: var(--font-color-alpha-20);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&:focus,
|
|
72
|
+
&:active,
|
|
73
|
+
&:focus-within,
|
|
74
|
+
&:has(:active) {
|
|
75
|
+
box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-accent-30);
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
&[data-error="true"] {
|
|
63
79
|
color: var(--color-red);
|
|
64
|
-
background-color: var(--alpha-red-10);
|
|
65
80
|
border-color: var(--alpha-red-10);
|
|
66
|
-
|
|
81
|
+
|
|
82
|
+
&:hover,
|
|
83
|
+
&:focus,
|
|
84
|
+
&:active,
|
|
85
|
+
&:focus-within,
|
|
86
|
+
&:has(:hover),
|
|
87
|
+
&:has(:active) {
|
|
88
|
+
background-color: var(--alpha-red-10);
|
|
89
|
+
box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
|
|
90
|
+
}
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
|
|
@@ -73,12 +97,17 @@ export const FieldVariantsStyles = css`
|
|
|
73
97
|
|
|
74
98
|
&:hover,
|
|
75
99
|
&:focus,
|
|
76
|
-
&:active
|
|
100
|
+
&:active,
|
|
101
|
+
&:focus-within,
|
|
102
|
+
&:has(:hover),
|
|
103
|
+
&:has(:active) {
|
|
77
104
|
border-color: var(--font-color-alpha-20);
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
&:focus,
|
|
81
|
-
&:active
|
|
108
|
+
&:active,
|
|
109
|
+
&:focus-within,
|
|
110
|
+
&:has(:active) {
|
|
82
111
|
box-shadow: 0 0 0 var(--measurement-small-30) var(--font-color-alpha-10);
|
|
83
112
|
}
|
|
84
113
|
|
|
@@ -88,7 +117,10 @@ export const FieldVariantsStyles = css`
|
|
|
88
117
|
|
|
89
118
|
&:hover,
|
|
90
119
|
&:focus,
|
|
91
|
-
&:active
|
|
120
|
+
&:active,
|
|
121
|
+
&:focus-within,
|
|
122
|
+
&:has(:hover),
|
|
123
|
+
&:has(:active) {
|
|
92
124
|
background-color: var(--alpha-red-10);
|
|
93
125
|
box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
|
|
94
126
|
}
|
|
@@ -105,7 +137,10 @@ export const FieldVariantsStyles = css`
|
|
|
105
137
|
|
|
106
138
|
&:hover,
|
|
107
139
|
&:focus,
|
|
108
|
-
&:active
|
|
140
|
+
&:active,
|
|
141
|
+
&:focus-within,
|
|
142
|
+
&:has(:hover),
|
|
143
|
+
&:has(:active) {
|
|
109
144
|
color: var(--font-color);
|
|
110
145
|
}
|
|
111
146
|
|
|
@@ -126,7 +161,6 @@ export const FieldSizeStyles = css`
|
|
|
126
161
|
padding: 0 var(--measurement-medium-30);
|
|
127
162
|
min-width: var(--measurement-medium-90);
|
|
128
163
|
min-height: var(--measurement-medium-90);
|
|
129
|
-
width: fit-content;
|
|
130
164
|
}
|
|
131
165
|
&[data-size="large"] {
|
|
132
166
|
padding: var(--measurement-medium-50);
|
|
@@ -143,6 +177,7 @@ export const FieldShapeStyles = css`
|
|
|
143
177
|
}
|
|
144
178
|
&[data-shape="round"] {
|
|
145
179
|
border-radius: var(--measurement-large-90);
|
|
180
|
+
padding-left: var(--measurement-medium-50) !important;
|
|
146
181
|
}
|
|
147
182
|
`;
|
|
148
183
|
|
|
@@ -190,3 +225,200 @@ export const Def = styled.dfn<any>`
|
|
|
190
225
|
}
|
|
191
226
|
}
|
|
192
227
|
`;
|
|
228
|
+
|
|
229
|
+
export const ParentContainer = styled.div<any>`
|
|
230
|
+
position: relative;
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: stretch;
|
|
233
|
+
width: 100%;
|
|
234
|
+
height: 100%;
|
|
235
|
+
|
|
236
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
237
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
238
|
+
-webkit-appearance: none;
|
|
239
|
+
margin: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
input {
|
|
243
|
+
width: 100% !important;
|
|
244
|
+
}
|
|
245
|
+
input[type="number"] {
|
|
246
|
+
appearance: textfield;
|
|
247
|
+
-moz-appearance: textfield;
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
export const ParentWrapper = styled.div<any>`
|
|
251
|
+
&[data-raw="false"] {
|
|
252
|
+
${FieldDefaultStyles}
|
|
253
|
+
${FieldVariantsStyles}
|
|
254
|
+
${FieldSizeStyles}
|
|
255
|
+
${FieldShapeStyles}
|
|
256
|
+
|
|
257
|
+
cursor: default;
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: start;
|
|
261
|
+
gap: var(--measurement-small-30);
|
|
262
|
+
width: 100% !important;
|
|
263
|
+
text-align: left !important;
|
|
264
|
+
|
|
265
|
+
&[data-error="true"] {
|
|
266
|
+
&::placeholder {
|
|
267
|
+
color: var(--alpha-red-30);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
&[data-wrap="true"] {
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
align-items: center;
|
|
274
|
+
align-content: center;
|
|
275
|
+
height: auto;
|
|
276
|
+
padding-top: var(--measurement-small-60);
|
|
277
|
+
padding-bottom: var(--measurement-small-60);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
export const InnerDivider = styled.div<any>`
|
|
283
|
+
height: var(--measurement-small-10);
|
|
284
|
+
width: 100%;
|
|
285
|
+
background-color: var(--font-color-alpha-10);
|
|
286
|
+
`;
|
|
287
|
+
export const InnerWrapper = styled.div<any>`
|
|
288
|
+
&[data-raw="false"] {
|
|
289
|
+
position: absolute;
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
|
|
293
|
+
right: 0;
|
|
294
|
+
&[data-multiple="true"] {
|
|
295
|
+
right: var(--measurement-small-10) !important;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
top: var(--measurement-small-10);
|
|
299
|
+
bottom: var(--measurement-small-10);
|
|
300
|
+
|
|
301
|
+
border-left: var(--measurement-small-10) solid var(--font-color-alpha-10);
|
|
302
|
+
border-color: var(--font-color-alpha-10);
|
|
303
|
+
|
|
304
|
+
overflow: hidden;
|
|
305
|
+
|
|
306
|
+
&[data-error="true"] {
|
|
307
|
+
border-color: var(--alpha-red-10) !important;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
&[data-shape="round"] {
|
|
311
|
+
border-radius: 0 var(--measurement-large-90) var(--measurement-large-90) 0;
|
|
312
|
+
}
|
|
313
|
+
&[data-shape="smooth"] {
|
|
314
|
+
border-radius: 0 var(--measurement-medium-20) var(--measurement-medium-20)
|
|
315
|
+
0;
|
|
316
|
+
}
|
|
317
|
+
&[data-shape="square"] {
|
|
318
|
+
border-radius: 0;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
export const InnerTrigger = styled.button<any>`
|
|
323
|
+
all: unset;
|
|
324
|
+
|
|
325
|
+
position: relative;
|
|
326
|
+
display: flex;
|
|
327
|
+
flex: 1;
|
|
328
|
+
|
|
329
|
+
align-items: center;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
box-sizing: border-box;
|
|
332
|
+
padding: 0 var(--measurement-medium-40);
|
|
333
|
+
|
|
334
|
+
color: var(--font-color-alpha-60);
|
|
335
|
+
|
|
336
|
+
background-color: var(--body-color);
|
|
337
|
+
background: linear-gradient(
|
|
338
|
+
180deg,
|
|
339
|
+
transparent 50%,
|
|
340
|
+
var(--font-color-alpha-10) 100%
|
|
341
|
+
);
|
|
342
|
+
background-size: 100% 200%;
|
|
343
|
+
background-position: 0% 0%;
|
|
344
|
+
backdrop-filter: blur(var(--measurement-medium-10));
|
|
345
|
+
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
transition: all ease-in-out 0.2s;
|
|
348
|
+
|
|
349
|
+
svg {
|
|
350
|
+
opacity: 0.6;
|
|
351
|
+
transition: all ease-in-out 0.2s;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
::before {
|
|
355
|
+
content: "";
|
|
356
|
+
inset: 0;
|
|
357
|
+
|
|
358
|
+
border-radius: inherit;
|
|
359
|
+
position: absolute;
|
|
360
|
+
box-sizing: border-box;
|
|
361
|
+
margin: 0;
|
|
362
|
+
padding: 0;
|
|
363
|
+
|
|
364
|
+
mask-composite: intersect;
|
|
365
|
+
|
|
366
|
+
transition: all ease-in-out 0.2s;
|
|
367
|
+
mask-image: linear-gradient(var(--font-color), transparent);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
&:hover,
|
|
371
|
+
&:active {
|
|
372
|
+
color: var(--font-color);
|
|
373
|
+
background-position: 0% 50%;
|
|
374
|
+
|
|
375
|
+
svg {
|
|
376
|
+
opacity: 0.8;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
&:disabled {
|
|
381
|
+
cursor: not-allowed;
|
|
382
|
+
opacity: 0.3;
|
|
383
|
+
}
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
export const InnerSegment = styled.span<any>`
|
|
387
|
+
&[data-raw="false"] {
|
|
388
|
+
border-radius: inherit;
|
|
389
|
+
|
|
390
|
+
text-align: center;
|
|
391
|
+
outline: none;
|
|
392
|
+
color: inherit;
|
|
393
|
+
transition: background-color ease-in-out 0.2s;
|
|
394
|
+
|
|
395
|
+
&[data-placeholder="true"] {
|
|
396
|
+
color: var(--font-color-alpha-30);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
&:hover,
|
|
400
|
+
&:focus,
|
|
401
|
+
&:active,
|
|
402
|
+
&:focus-within,
|
|
403
|
+
&:has(:active) {
|
|
404
|
+
background-color: var(--font-color-alpha-10);
|
|
405
|
+
color: var(--font-color);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
export const Muted = styled.span<any>`
|
|
410
|
+
&[data-raw="false"] {
|
|
411
|
+
color: var(--font-color-alpha-30);
|
|
412
|
+
user-select: none;
|
|
413
|
+
}
|
|
414
|
+
`;
|
|
415
|
+
export const HiddenInput = styled.input<any>`
|
|
416
|
+
border: none;
|
|
417
|
+
outline: none;
|
|
418
|
+
background: transparent;
|
|
419
|
+
flex: 1;
|
|
420
|
+
font: inherit;
|
|
421
|
+
color: inherit;
|
|
422
|
+
padding: 0;
|
|
423
|
+
min-width: var(--measurement-medium-60);
|
|
424
|
+
`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type TDateSegmentType =
|
|
2
|
+
| "day"
|
|
3
|
+
| "month"
|
|
4
|
+
| "year"
|
|
5
|
+
| "hour"
|
|
6
|
+
| "minute"
|
|
7
|
+
| "literal";
|
|
8
|
+
|
|
9
|
+
export interface ISegment {
|
|
10
|
+
type: TDateSegmentType;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DateState {
|
|
15
|
+
day: number;
|
|
16
|
+
month: number;
|
|
17
|
+
year: number;
|
|
18
|
+
hour: number;
|
|
19
|
+
minute: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SegmentRanges: Record<
|
|
23
|
+
Exclude<TDateSegmentType, "literal">,
|
|
24
|
+
{ min: number; max: (date: DateState) => number }
|
|
25
|
+
> = {
|
|
26
|
+
day: { min: 1, max: () => 31 },
|
|
27
|
+
month: { min: 1, max: () => 12 },
|
|
28
|
+
year: { min: 1, max: () => 9999 },
|
|
29
|
+
hour: { min: 0, max: () => 23 },
|
|
30
|
+
minute: { min: 0, max: () => 59 },
|
|
31
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SegmentRanges,
|
|
3
|
+
type DateState,
|
|
4
|
+
type ISegment,
|
|
5
|
+
type TDateSegmentType,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a `Date` object into a flat `DateState` record.
|
|
10
|
+
*
|
|
11
|
+
* Month is normalized from the zero-based `Date` API (0–11) to a
|
|
12
|
+
* human-readable value (1–12) so that segment values always match
|
|
13
|
+
* what is displayed to the user.
|
|
14
|
+
*
|
|
15
|
+
* @param date - The `Date` instance to decompose.
|
|
16
|
+
* @returns A `DateState` object with individual `day`, `month`, `year`,
|
|
17
|
+
* `hour`, and `minute` fields.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* dateToState(new Date(2026, 2, 28, 9, 54));
|
|
21
|
+
* // → { day: 28, month: 3, year: 2026, hour: 9, minute: 54 }
|
|
22
|
+
*/
|
|
23
|
+
export function dateToState(date: Date): DateState {
|
|
24
|
+
return {
|
|
25
|
+
day: date.getDate(),
|
|
26
|
+
month: date.getMonth() + 1, // Normalize: Date months are 0-indexed
|
|
27
|
+
year: date.getFullYear(),
|
|
28
|
+
hour: date.getHours(),
|
|
29
|
+
minute: date.getMinutes(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconstructs a `Date` object from a flat `DateState` record.
|
|
35
|
+
*
|
|
36
|
+
* Month is converted back from human-readable (1–12) to the zero-based
|
|
37
|
+
* value expected by the `Date` constructor.
|
|
38
|
+
*
|
|
39
|
+
* @param state - The `DateState` to recompose.
|
|
40
|
+
* @returns A `Date` instance representing the given state.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* stateToDate({ day: 28, month: 3, year: 2026, hour: 9, minute: 54 });
|
|
44
|
+
* // → Date { Sat Mar 28 2026 09:54:00 }
|
|
45
|
+
*/
|
|
46
|
+
export function stateToDate(state: DateState): Date {
|
|
47
|
+
return new Date(
|
|
48
|
+
state.year,
|
|
49
|
+
state.month - 1, // Normalize: Date constructor expects 0-indexed months
|
|
50
|
+
state.day,
|
|
51
|
+
state.hour,
|
|
52
|
+
state.minute,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds an ordered array of display segments for the date (and optionally
|
|
58
|
+
* time) input, using `Intl.DateTimeFormat.formatToParts` to resolve the
|
|
59
|
+
* correct part order for the given locale.
|
|
60
|
+
*
|
|
61
|
+
* Each segment is either an editable unit (`day`, `month`, `year`, `hour`,
|
|
62
|
+
* `minute`) or a non-interactive separator (`literal`). Empty literals are
|
|
63
|
+
* filtered out to avoid rendering invisible nodes.
|
|
64
|
+
*
|
|
65
|
+
* When `withTime` is `true`, time segments are appended after a double-space
|
|
66
|
+
* literal separator so they can be visually distinguished from date segments
|
|
67
|
+
* without introducing a hard-coded character.
|
|
68
|
+
*
|
|
69
|
+
* @param state - Current `DateState` used as the formatting probe date.
|
|
70
|
+
* @param locale - BCP 47 locale tag (e.g. `"en-US"`, `"fr-FR"`) that controls
|
|
71
|
+
* the segment order returned by `Intl.DateTimeFormat`.
|
|
72
|
+
* @param withTime - When `true`, hour and minute segments are appended.
|
|
73
|
+
* @returns An ordered `ISegment[]` array ready to be rendered.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // French locale → day before month
|
|
77
|
+
* buildSegments({ day: 28, month: 3, year: 2026, hour: 9, minute: 54 }, "fr-FR", true);
|
|
78
|
+
* // → [
|
|
79
|
+
* // { type: "day", value: "28" },
|
|
80
|
+
* // { type: "literal", value: "/" },
|
|
81
|
+
* // { type: "month", value: "03" },
|
|
82
|
+
* // { type: "literal", value: "/" },
|
|
83
|
+
* // { type: "year", value: "2026" },
|
|
84
|
+
* // { type: "literal", value: " " }, ← date/time separator
|
|
85
|
+
* // { type: "hour", value: "09" },
|
|
86
|
+
* // { type: "literal", value: ":" },
|
|
87
|
+
* // { type: "minute", value: "54" },
|
|
88
|
+
* // ]
|
|
89
|
+
*/
|
|
90
|
+
export function buildSegments(
|
|
91
|
+
state: DateState,
|
|
92
|
+
locale: string,
|
|
93
|
+
withTime: boolean,
|
|
94
|
+
): ISegment[] {
|
|
95
|
+
// Format only the date parts (day / month / year) in locale order
|
|
96
|
+
const dateFormatter = new Intl.DateTimeFormat(locale, {
|
|
97
|
+
day: "2-digit",
|
|
98
|
+
month: "2-digit",
|
|
99
|
+
year: "numeric",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Format only the time parts (hour / minute) using 24-hour clock so
|
|
103
|
+
// we never have to deal with am/pm segments
|
|
104
|
+
const timeFormatter = new Intl.DateTimeFormat(locale, {
|
|
105
|
+
hour: "2-digit",
|
|
106
|
+
minute: "2-digit",
|
|
107
|
+
hour12: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Use the current state as the probe date so formatted values match
|
|
111
|
+
// what is actually stored
|
|
112
|
+
const probe = stateToDate(state);
|
|
113
|
+
|
|
114
|
+
const dateParts = dateFormatter
|
|
115
|
+
.formatToParts(probe)
|
|
116
|
+
// Discard whitespace-only literals to avoid invisible DOM nodes
|
|
117
|
+
.filter((p) => p.type !== "literal" || p.value.trim() !== "")
|
|
118
|
+
.map((p): ISegment => {
|
|
119
|
+
if (p.type === "day") return { type: "day", value: p.value };
|
|
120
|
+
if (p.type === "month") return { type: "month", value: p.value };
|
|
121
|
+
if (p.type === "year") return { type: "year", value: p.value };
|
|
122
|
+
|
|
123
|
+
// Any remaining part (e.g. "/" or ".") becomes a non-interactive literal
|
|
124
|
+
return { type: "literal", value: p.value };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Return date-only segments when time display is not requested
|
|
128
|
+
if (!withTime) return dateParts;
|
|
129
|
+
|
|
130
|
+
const timeParts = timeFormatter
|
|
131
|
+
.formatToParts(probe)
|
|
132
|
+
.filter((p) => p.type !== "literal" || p.value.trim() !== "")
|
|
133
|
+
.map((p): ISegment => {
|
|
134
|
+
if (p.type === "hour") return { type: "hour", value: p.value };
|
|
135
|
+
if (p.type === "minute") return { type: "minute", value: p.value };
|
|
136
|
+
return { type: "literal", value: p.value };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Separate date and time groups with a double-space literal so they are
|
|
140
|
+
// visually distinct without coupling to any locale-specific character
|
|
141
|
+
return [...dateParts, { type: "literal", value: " " }, ...timeParts];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Applies a new `DateState` to both the internal React state and the
|
|
146
|
+
* external `onChange` callback, respecting the controlled / uncontrolled
|
|
147
|
+
* pattern.
|
|
148
|
+
*
|
|
149
|
+
* - **Uncontrolled**: updates `internalState` directly so the component
|
|
150
|
+
* re-renders with the new value.
|
|
151
|
+
* - **Controlled**: skips the internal update and lets the parent drive the
|
|
152
|
+
* value through the `onChange` → `value` prop cycle.
|
|
153
|
+
* - In both modes, `onChange` is always called when provided, so consumers
|
|
154
|
+
* receive every change regardless of control mode.
|
|
155
|
+
*
|
|
156
|
+
* @param isControlled - Whether the component is in controlled mode
|
|
157
|
+
* (`value` prop is defined).
|
|
158
|
+
* @param next - The new `DateState` to commit.
|
|
159
|
+
* @param setInternalState - React `useState` setter for the internal state.
|
|
160
|
+
* @param onChange - Optional external change handler provided by the consumer.
|
|
161
|
+
*/
|
|
162
|
+
export const commitState = (
|
|
163
|
+
isControlled: boolean,
|
|
164
|
+
next: DateState,
|
|
165
|
+
setInternalState: (value: React.SetStateAction<DateState>) => void,
|
|
166
|
+
onChange: ((date: Date) => void) | undefined,
|
|
167
|
+
) => {
|
|
168
|
+
// Only mutate internal state when uncontrolled; controlled components rely
|
|
169
|
+
// on the parent re-rendering with the updated `value` prop
|
|
170
|
+
if (!isControlled) setInternalState(next);
|
|
171
|
+
|
|
172
|
+
// Always fire onChange so the consumer can react in both modes
|
|
173
|
+
onChange?.(stateToDate(next));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Clamps a raw numeric value to the valid range for a given date/time segment.
|
|
178
|
+
*
|
|
179
|
+
* Ranges are looked up from `SegmentRanges`, whose `max` functions accept the
|
|
180
|
+
* full `DateState` so month-aware day limits (e.g., 28 for February) can be
|
|
181
|
+
* applied in the future without changing this function's signature.
|
|
182
|
+
*
|
|
183
|
+
* @param val - The raw number to clamp (e.g. from a keyboard buffer).
|
|
184
|
+
* @param seg - The target segment type (excludes `"literal"`).
|
|
185
|
+
* @param internalState - The current `DateState`, forwarded to the `max`
|
|
186
|
+
* function so context-sensitive limits can be computed.
|
|
187
|
+
* @returns The value clamped to `[min, max]` for the given segment.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* clamp(45, "month", state); // → 12
|
|
191
|
+
* clamp(0, "day", state); // → 1
|
|
192
|
+
* clamp(7, "hour", state); // → 7
|
|
193
|
+
*/
|
|
194
|
+
export const clamp = (
|
|
195
|
+
val: number,
|
|
196
|
+
seg: Exclude<TDateSegmentType, "literal">,
|
|
197
|
+
internalState: DateState,
|
|
198
|
+
): number => {
|
|
199
|
+
const { min, max } = SegmentRanges[seg];
|
|
200
|
+
return Math.min(Math.max(val, min), max(internalState));
|
|
201
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ export * from "./otp-field";
|
|
|
16
16
|
export * from "./overlay";
|
|
17
17
|
export * from "./page";
|
|
18
18
|
export * from "./portal";
|
|
19
|
-
export * from "./privacy-field";
|
|
20
19
|
export * from "./resizable";
|
|
21
20
|
export * from "./sheet";
|
|
22
21
|
export * from "./shimmer";
|
|
@@ -31,6 +30,7 @@ export * from "./toggle";
|
|
|
31
30
|
export * from "./toolbar";
|
|
32
31
|
export * from "./tooltip";
|
|
33
32
|
export * from "./tree";
|
|
33
|
+
export * from "./select";
|
|
34
34
|
|
|
35
35
|
export { useAccordion } from "./accordion/hooks";
|
|
36
36
|
export { useCheckbox } from "./checkbox/hooks";
|
|
@@ -45,3 +45,4 @@ export { useToolbar } from "./toolbar/hooks";
|
|
|
45
45
|
export { useMessageBubble } from "./message-bubble/hooks";
|
|
46
46
|
export { useTree } from "./tree/hooks/tree-provider";
|
|
47
47
|
export { useTreeNode } from "./tree/hooks/tree-node-provider";
|
|
48
|
+
export { useSelect } from "./select/hooks";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import { MessageBubble } from "..";
|
|
4
|
+
import { Avatar, Field, MessageBubble, Page, ScrollArea, Textarea } from "..";
|
|
5
5
|
|
|
6
6
|
const meta = {
|
|
7
7
|
title: "Components/MessageBubble",
|
|
@@ -9,9 +9,11 @@ const meta = {
|
|
|
9
9
|
tags: ["autodocs"],
|
|
10
10
|
decorators: [
|
|
11
11
|
(Story) => (
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
12
|
+
<Page>
|
|
13
|
+
<Page.Content className="p-medium-30">
|
|
14
|
+
<Story />
|
|
15
|
+
</Page.Content>
|
|
16
|
+
</Page>
|
|
15
17
|
),
|
|
16
18
|
],
|
|
17
19
|
} satisfies Meta<typeof MessageBubble>;
|
|
@@ -70,22 +72,67 @@ export const Right: Story = {
|
|
|
70
72
|
|
|
71
73
|
export const Conversation: Story = {
|
|
72
74
|
render: () => (
|
|
73
|
-
<
|
|
75
|
+
<ScrollArea className="h-100 w-100 flex flex-column g-medium-30 " scrollbar>
|
|
74
76
|
{(
|
|
75
77
|
[
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
{
|
|
79
|
+
variant: "border",
|
|
80
|
+
side: "left",
|
|
81
|
+
message: "Hey, how are you doing?",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
variant: "primary",
|
|
85
|
+
side: "right",
|
|
86
|
+
message: "All good! What about you?",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
variant: "border",
|
|
90
|
+
side: "left",
|
|
91
|
+
message: "Pretty great, thanks for asking",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
variant: "primary",
|
|
95
|
+
side: "right",
|
|
96
|
+
message:
|
|
97
|
+
"Hic dolorum esse magnam sint quibusdam porro reprehenderit, enim, repellendus ipsam, iste est! Deserunt ipsam ullam dolores expedita rem, magni iste eveniet.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
variant: "hint",
|
|
101
|
+
side: "right",
|
|
102
|
+
message: "Hic dolorum esse magnam sint quibusdam.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
variant: "meta",
|
|
106
|
+
side: "right",
|
|
107
|
+
message: "Ipsa nisi fugiat doloribus.",
|
|
108
|
+
},
|
|
80
109
|
] as const
|
|
81
|
-
).map(({ side, message }, index) => (
|
|
110
|
+
).map(({ variant, side, message }, index) => (
|
|
82
111
|
<MessageBubble.Root key={index}>
|
|
83
112
|
<MessageBubble side={side}>
|
|
84
|
-
|
|
113
|
+
{side === "left" && (
|
|
114
|
+
<Field.Meta variant="hint">
|
|
115
|
+
<Avatar
|
|
116
|
+
sizing="small"
|
|
117
|
+
alt="foundation-logo"
|
|
118
|
+
src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
|
|
119
|
+
shape="smooth"
|
|
120
|
+
>
|
|
121
|
+
<Avatar.Badge
|
|
122
|
+
alt="foundation-logo"
|
|
123
|
+
src="https://www.untitledui.com/logos/images/Layers.jpg"
|
|
124
|
+
/>
|
|
125
|
+
</Avatar>
|
|
126
|
+
</Field.Meta>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
<MessageBubble.Content variant={variant} className="fs-medium-20">
|
|
130
|
+
{message}
|
|
131
|
+
</MessageBubble.Content>
|
|
85
132
|
<MessageBubble.Meta createdAt={MOCK_DATE} />
|
|
86
133
|
</MessageBubble>
|
|
87
134
|
</MessageBubble.Root>
|
|
88
135
|
))}
|
|
89
|
-
</
|
|
136
|
+
</ScrollArea>
|
|
90
137
|
),
|
|
91
138
|
};
|