@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +380 -52
  3. package/dist/index.d.ts +380 -52
  4. package/dist/index.js +2532 -511
  5. package/dist/index.mjs +2518 -508
  6. package/package.json +3 -3
  7. package/src/__tests__/Avatar.test.tsx +55 -55
  8. package/src/accordion/Accordion.stories.tsx +6 -4
  9. package/src/accordion/index.tsx +1 -2
  10. package/src/avatar/Avatar.stories.tsx +37 -7
  11. package/src/avatar/index.tsx +90 -19
  12. package/src/avatar/styles/index.ts +58 -12
  13. package/src/badge/Badge.stories.tsx +27 -5
  14. package/src/badge/index.tsx +21 -13
  15. package/src/badge/styles/index.ts +69 -40
  16. package/src/button/Button.stories.tsx +40 -27
  17. package/src/button/index.tsx +13 -9
  18. package/src/button/styles/index.ts +308 -47
  19. package/src/card/index.tsx +2 -4
  20. package/src/checkbox/Checkbox.stories.tsx +72 -33
  21. package/src/checkbox/index.tsx +8 -6
  22. package/src/checkbox/styles/index.ts +239 -19
  23. package/src/collapsible/Collapsible.stories.tsx +6 -4
  24. package/src/dialog/Dialog.stories.tsx +173 -31
  25. package/src/dialog/styles/index.ts +13 -8
  26. package/src/dropdown/Dropdown.stories.tsx +61 -23
  27. package/src/dropdown/index.tsx +42 -31
  28. package/src/dropdown/styles/index.ts +30 -19
  29. package/src/field/Field.stories.tsx +183 -24
  30. package/src/field/index.tsx +930 -13
  31. package/src/field/styles/index.ts +246 -14
  32. package/src/field/types/index.ts +31 -0
  33. package/src/field/utils/index.ts +201 -0
  34. package/src/index.ts +2 -1
  35. package/src/message-bubble/MessageBubble.stories.tsx +59 -12
  36. package/src/message-bubble/index.tsx +22 -4
  37. package/src/message-bubble/styles/index.ts +4 -7
  38. package/src/otp-field/OTPField.stories.tsx +22 -24
  39. package/src/otp-field/index.tsx +9 -0
  40. package/src/otp-field/styles/index.ts +114 -16
  41. package/src/otp-field/types/index.ts +9 -1
  42. package/src/overlay/styles/index.ts +1 -0
  43. package/src/ruler/Ruler.stories.tsx +43 -0
  44. package/src/ruler/constants/index.ts +3 -0
  45. package/src/ruler/hooks/index.tsx +53 -0
  46. package/src/ruler/index.tsx +239 -0
  47. package/src/ruler/styles/index.tsx +154 -0
  48. package/src/ruler/types/index.ts +17 -0
  49. package/src/select/Select.stories.tsx +91 -0
  50. package/src/select/hooks/index.tsx +71 -0
  51. package/src/select/index.tsx +331 -0
  52. package/src/select/styles/index.tsx +156 -0
  53. package/src/shimmer/Shimmer.stories.tsx +6 -4
  54. package/src/skeleton/index.tsx +7 -6
  55. package/src/spinner/Spinner.stories.tsx +29 -4
  56. package/src/spinner/index.tsx +16 -6
  57. package/src/spinner/styles/index.ts +41 -22
  58. package/src/switch/Switch.stories.tsx +46 -17
  59. package/src/switch/index.tsx +5 -8
  60. package/src/switch/styles/index.ts +45 -45
  61. package/src/tabs/Tabs.stories.tsx +43 -15
  62. package/src/text-area/Textarea.stories.tsx +45 -8
  63. package/src/text-area/index.tsx +9 -6
  64. package/src/text-area/styles/index.ts +1 -1
  65. package/src/toggle/Toggle.stories.tsx +6 -4
  66. package/src/tree/Tree.stories.tsx +6 -4
  67. package/src/privacy-field/PrivacyField.stories.tsx +0 -29
  68. package/src/privacy-field/index.tsx +0 -56
  69. 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.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
- width: fit-content;
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: var(--font-color-alpha-10);
59
+ background-color: transparent;
55
60
  border-color: var(--font-color-alpha-10);
56
61
 
62
+ &:hover,
57
63
  &:focus,
58
- &:active {
59
- box-shadow: 0 0 0 var(--measurement-small-30) var(--font-color-alpha-10);
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
- box-shadow: 0 0 0 var(--measurement-small-30) var(--alpha-red-10);
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
- <div className="m-medium-30">
13
- <Story />
14
- </div>
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
- <React.Fragment>
75
+ <ScrollArea className="h-100 w-100 flex flex-column g-medium-30 " scrollbar>
74
76
  {(
75
77
  [
76
- { side: "left", message: "Hey, how are you doing?" },
77
- { side: "right", message: "All good! What about you?" },
78
- { side: "left", message: "Pretty great, thanks for asking 🐻" },
79
- { side: "right", message: "Glad to hear it! 🐻❄️" },
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
- <MessageBubble.Content>{message}</MessageBubble.Content>
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
- </React.Fragment>
136
+ </ScrollArea>
90
137
  ),
91
138
  };