@scripso-homepad/ui 0.4.10 → 0.4.11

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 (37) hide show
  1. package/dist/index.cjs +122 -43
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +2 -2
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +124 -45
  6. package/dist/index.js.map +1 -1
  7. package/dist/web/index.cjs +45 -33
  8. package/dist/web/index.cjs.map +1 -1
  9. package/dist/web/index.d.cts +2 -1
  10. package/dist/web/index.d.ts +2 -1
  11. package/dist/web/index.js +45 -33
  12. package/dist/web/index.js.map +1 -1
  13. package/package.json +3 -2
  14. package/src/components/Calendar.tsx +10 -2
  15. package/src/components/DatePicker.tsx +3 -3
  16. package/src/components/Select.tsx +133 -38
  17. package/src/theme/select.ts +1 -1
  18. package/src/utils/calendarDays.ts +15 -6
  19. package/src/web/components/Badge.tsx +1 -1
  20. package/src/web/components/ConfirmModal.tsx +2 -2
  21. package/src/web/components/Drawer.stories.tsx +7 -7
  22. package/src/web/components/Drawer.tsx +2 -2
  23. package/src/web/components/HistoryTimeline.tsx +2 -2
  24. package/src/web/components/Modal.stories.tsx +1 -1
  25. package/src/web/components/Modal.tsx +27 -10
  26. package/src/web/components/Pagination.tsx +2 -2
  27. package/src/web/components/TableActionButton.tsx +1 -1
  28. package/src/web/components/TableActionsMenu.tsx +1 -1
  29. package/src/web/components/TableIconText.tsx +1 -1
  30. package/src/web/components/Toaster.stories.tsx +7 -7
  31. package/src/web/components/table/constants.ts +2 -2
  32. package/src/web/layout/AppHeader.tsx +1 -1
  33. package/src/web/layout/BuildingSelect.tsx +2 -2
  34. package/src/web/layout/DashboardLayout.stories.tsx +1 -1
  35. package/src/web/layout/SidebarNavItem.tsx +1 -1
  36. package/src/web/layout/SidebarUserCard.tsx +3 -3
  37. package/src/web/styles/typography.css +99 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -30,7 +30,8 @@
30
30
  "require": "./dist/web/index.cjs",
31
31
  "default": "./src/web/index.ts"
32
32
  },
33
- "./web/table-scroll.css": "./src/web/components/table-scroll.css"
33
+ "./web/table-scroll.css": "./src/web/components/table-scroll.css",
34
+ "./web/typography.css": "./src/web/styles/typography.css"
34
35
  },
35
36
  "files": [
36
37
  "dist",
@@ -13,6 +13,7 @@ import {
13
13
  formatCalendarYear,
14
14
  getYearPageStart,
15
15
  isMonthDisabled,
16
+ isSameDay,
16
17
  isYearDisabled,
17
18
  selectRangeDate,
18
19
  type CalendarDayCell,
@@ -64,13 +65,13 @@ type CalendarBaseProps = {
64
65
  export type CalendarSingleProps = CalendarBaseProps & {
65
66
  mode?: "single";
66
67
  value?: Date;
67
- onChange?: (date: Date) => void;
68
+ onChange?: (date: Date | undefined) => void;
68
69
  };
69
70
 
70
71
  export type CalendarRangeProps = CalendarBaseProps & {
71
72
  mode: "range";
72
73
  value?: DateRange;
73
- onChange?: (range: DateRange) => void;
74
+ onChange?: (range: DateRange | undefined) => void;
74
75
  };
75
76
 
76
77
  export type CalendarProps = CalendarSingleProps | CalendarRangeProps;
@@ -275,6 +276,13 @@ export function Calendar(props: CalendarProps) {
275
276
  return;
276
277
  }
277
278
 
279
+ const currentValue = singleProps?.value;
280
+
281
+ if (currentValue && isSameDay(date, currentValue)) {
282
+ singleProps?.onChange?.(undefined);
283
+ return;
284
+ }
285
+
278
286
  singleProps?.onChange?.(date);
279
287
  }
280
288
 
@@ -207,7 +207,7 @@ export function DatePicker(props: DatePickerProps) {
207
207
  });
208
208
  }
209
209
 
210
- function handleSingleSelect(date: Date) {
210
+ function handleSingleSelect(date: Date | undefined) {
211
211
  if (props.mode === "range") {
212
212
  return;
213
213
  }
@@ -216,14 +216,14 @@ export function DatePicker(props: DatePickerProps) {
216
216
  closePicker();
217
217
  }
218
218
 
219
- function handleRangeSelect(range: DateRange) {
219
+ function handleRangeSelect(range: DateRange | undefined) {
220
220
  if (props.mode !== "range") {
221
221
  return;
222
222
  }
223
223
 
224
224
  props.onChange?.(range);
225
225
 
226
- if (closeOnRangeComplete && range.end) {
226
+ if (closeOnRangeComplete && range?.end) {
227
227
  closePicker();
228
228
  }
229
229
  }
@@ -73,6 +73,112 @@ function renderSelectIcon(icon: ReactNode, color: string) {
73
73
  return icon;
74
74
  }
75
75
 
76
+ function resolveWebTextNode(
77
+ ref: React.RefObject<ComponentRef<typeof Text> | null>,
78
+ ): HTMLElement | null {
79
+ if (Platform.OS !== "web") {
80
+ return null;
81
+ }
82
+
83
+ const node = ref.current as unknown;
84
+ return node instanceof HTMLElement ? node : null;
85
+ }
86
+
87
+ function syncTruncatedNativeTitle(
88
+ ref: React.RefObject<ComponentRef<typeof Text> | null>,
89
+ label: string,
90
+ ): void {
91
+ const element = resolveWebTextNode(ref);
92
+ if (!element) {
93
+ return;
94
+ }
95
+
96
+ if (element.scrollWidth > element.clientWidth) {
97
+ element.setAttribute("title", label);
98
+ return;
99
+ }
100
+
101
+ element.removeAttribute("title");
102
+ }
103
+
104
+ function clearNativeTitle(ref: React.RefObject<ComponentRef<typeof Text> | null>): void {
105
+ resolveWebTextNode(ref)?.removeAttribute("title");
106
+ }
107
+
108
+ interface SelectOptionItemProps {
109
+ option: SelectOption;
110
+ compact: boolean;
111
+ optionHeight: number;
112
+ isSelected: boolean;
113
+ isHighlighted: boolean;
114
+ isLast: boolean;
115
+ selectableIndex: number;
116
+ onHighlight: (selectableIndex: number) => void;
117
+ onSelect: (value: string) => void;
118
+ }
119
+
120
+ function SelectOptionItem({
121
+ option,
122
+ compact,
123
+ optionHeight,
124
+ isSelected,
125
+ isHighlighted,
126
+ isLast,
127
+ selectableIndex,
128
+ onHighlight,
129
+ onSelect,
130
+ }: SelectOptionItemProps) {
131
+ const labelRef = useRef<ComponentRef<typeof Text>>(null);
132
+
133
+ return (
134
+ <Pressable
135
+ accessibilityRole="button"
136
+ accessibilityState={{ disabled: option.disabled, selected: isSelected }}
137
+ disabled={option.disabled}
138
+ onPress={() => {
139
+ if (!option.disabled) {
140
+ onSelect(option.value);
141
+ }
142
+ }}
143
+ onHoverIn={() => {
144
+ if (!option.disabled && selectableIndex >= 0) {
145
+ onHighlight(selectableIndex);
146
+ }
147
+
148
+ syncTruncatedNativeTitle(labelRef, option.label);
149
+ }}
150
+ onHoverOut={() => {
151
+ clearNativeTitle(labelRef);
152
+ }}
153
+ style={[
154
+ selectDropdownMetrics.option,
155
+ compact && styles.optionCompact,
156
+ { height: optionHeight, minHeight: optionHeight, maxHeight: optionHeight },
157
+ isSelected && selectDropdownMetrics.optionSelected,
158
+ compact && isSelected && styles.optionSelectedCompact,
159
+ isHighlighted && !isSelected && !option.disabled
160
+ ? selectDropdownMetrics.optionHighlighted
161
+ : null,
162
+ !isLast && styles.optionSpacing,
163
+ option.disabled && styles.optionDisabled,
164
+ ]}
165
+ >
166
+ <Text
167
+ ref={labelRef}
168
+ style={[
169
+ selectDropdownMetrics.optionLabel,
170
+ styles.optionLabel,
171
+ isSelected && selectDropdownMetrics.optionLabelSelected,
172
+ option.disabled && styles.optionLabelDisabled,
173
+ ]}
174
+ numberOfLines={1}
175
+ >
176
+ {option.label}
177
+ </Text>
178
+ </Pressable>
179
+ );
180
+ }
181
+
76
182
  export type SelectOption = {
77
183
  value: string;
78
184
  label: string;
@@ -282,6 +388,12 @@ export function Select({
282
388
  return;
283
389
  }
284
390
 
391
+ if (selectedValues.includes(nextValue)) {
392
+ (onValueChange as SingleSelectProps["onValueChange"] | undefined)?.("");
393
+ closeMenu();
394
+ return;
395
+ }
396
+
285
397
  (onValueChange as SingleSelectProps["onValueChange"] | undefined)?.(nextValue);
286
398
  closeMenu();
287
399
  }
@@ -293,45 +405,18 @@ export function Select({
293
405
  const isLast = index === options.length - 1;
294
406
 
295
407
  return (
296
- <Pressable
408
+ <SelectOptionItem
297
409
  key={option.value}
298
- accessibilityRole="button"
299
- accessibilityState={{ disabled: option.disabled, selected: isSelected }}
300
- disabled={option.disabled}
301
- onPress={() => {
302
- if (!option.disabled) {
303
- handleSelect(option.value);
304
- }
305
- }}
306
- onHoverIn={() => {
307
- if (!option.disabled && selectableIndex >= 0) {
308
- setHighlightedIndex(selectableIndex);
309
- }
310
- }}
311
- style={[
312
- selectDropdownMetrics.option,
313
- compact && styles.optionCompact,
314
- { height: optionHeight, minHeight: optionHeight, maxHeight: optionHeight },
315
- isSelected && selectDropdownMetrics.optionSelected,
316
- compact && isSelected && styles.optionSelectedCompact,
317
- isHighlighted && !isSelected && !option.disabled
318
- ? selectDropdownMetrics.optionHighlighted
319
- : null,
320
- !isLast && styles.optionSpacing,
321
- option.disabled && styles.optionDisabled,
322
- ]}
323
- >
324
- <Text
325
- style={[
326
- selectDropdownMetrics.optionLabel,
327
- isSelected && selectDropdownMetrics.optionLabelSelected,
328
- option.disabled && styles.optionLabelDisabled,
329
- ]}
330
- numberOfLines={1}
331
- >
332
- {option.label}
333
- </Text>
334
- </Pressable>
410
+ option={option}
411
+ compact={compact}
412
+ optionHeight={optionHeight}
413
+ isSelected={isSelected}
414
+ isHighlighted={isHighlighted}
415
+ isLast={isLast}
416
+ selectableIndex={selectableIndex}
417
+ onHighlight={setHighlightedIndex}
418
+ onSelect={handleSelect}
419
+ />
335
420
  );
336
421
  });
337
422
 
@@ -479,6 +564,16 @@ const styles = StyleSheet.create({
479
564
  optionDisabled: {
480
565
  opacity: 0.5,
481
566
  },
567
+ optionLabel: {
568
+ minWidth: 0,
569
+ ...(Platform.OS === "web"
570
+ ? ({
571
+ overflow: "hidden",
572
+ textOverflow: "ellipsis",
573
+ whiteSpace: "nowrap",
574
+ } as const)
575
+ : null),
576
+ },
482
577
  optionLabelDisabled: {
483
578
  color: colors.stormGray300,
484
579
  },
@@ -29,7 +29,7 @@ export const selectLabelTypography = {
29
29
  fontFamily: fonts.sans,
30
30
  fontSize: fontSize.sm,
31
31
  fontWeight: fontWeight.medium,
32
- lineHeight: fontSize.sm,
32
+ lineHeight: 16,
33
33
  letterSpacing: 0,
34
34
  color: colors.slateBlue,
35
35
  } as const;
@@ -41,24 +41,33 @@ export function isInDateRange(date: Date, start?: Date, end?: Date): boolean {
41
41
  return !isBeforeDay(day, start) && !isAfterDay(day, end);
42
42
  }
43
43
 
44
- export function selectRangeDate(clicked: Date, current?: DateRange): DateRange {
44
+ export function selectRangeDate(clicked: Date, current?: DateRange): DateRange | undefined {
45
45
  const clickedDate = stripTime(clicked);
46
46
 
47
- if (!current?.start || current.end) {
47
+ if (!current?.start) {
48
48
  return { start: clickedDate };
49
49
  }
50
50
 
51
51
  const start = stripTime(current.start);
52
+ const end = current.end ? stripTime(current.end) : undefined;
53
+
54
+ if (!end) {
55
+ if (isSameDay(clickedDate, start)) {
56
+ return undefined;
57
+ }
58
+
59
+ if (isBeforeDay(clickedDate, start)) {
60
+ return { start: clickedDate };
61
+ }
52
62
 
53
- if (isSameDay(clickedDate, start)) {
54
63
  return { start, end: clickedDate };
55
64
  }
56
65
 
57
- if (isBeforeDay(clickedDate, start)) {
58
- return { start: clickedDate };
66
+ if (isSameDay(clickedDate, end)) {
67
+ return undefined;
59
68
  }
60
69
 
61
- return { start, end: clickedDate };
70
+ return { start: clickedDate };
62
71
  }
63
72
 
64
73
  export function isSameDay(left: Date, right: Date): boolean {
@@ -23,7 +23,7 @@ export function Badge({ children, variant = 'neutral', icon, className }: BadgeP
23
23
  return (
24
24
  <span
25
25
  className={cn(
26
- 'inline-flex items-center gap-2 rounded-[28px] px-3 py-1.5 font-sans text-xs font-semibold leading-none whitespace-nowrap',
26
+ 'inline-flex items-center gap-2 rounded-[28px] px-3 py-1.5 typography-12 font-semibold whitespace-nowrap',
27
27
  badgeVariantClasses[variant],
28
28
  className,
29
29
  )}
@@ -78,13 +78,13 @@ export function ConfirmModal({
78
78
  <header className="flex flex-col gap-3 text-center">
79
79
  <h2
80
80
  id="homepad-confirm-modal-title"
81
- className="font-sans text-xl font-bold leading-6.75 tracking-normal text-black"
81
+ className="typography-title tracking-normal text-black"
82
82
  >
83
83
  {title}
84
84
  </h2>
85
85
  <div
86
86
  id="homepad-confirm-modal-description"
87
- className="text-center font-sans text-sm font-normal leading-4.75 tracking-normal text-storm-gray-400"
87
+ className="text-center typography-body tracking-normal text-storm-gray-400"
88
88
  >
89
89
  {description}
90
90
  </div>
@@ -44,10 +44,10 @@ type Story = StoryObj<typeof meta>;
44
44
  function DetailField({ label, value }: { label: string; value: string }) {
45
45
  return (
46
46
  <div className="flex items-center justify-between gap-4">
47
- <span className="min-w-0 flex-1 font-sans text-sm font-medium leading-[19px] tracking-normal text-storm-gray-500">
47
+ <span className="min-w-0 flex-1 typography-14 font-medium tracking-normal text-storm-gray-500">
48
48
  {label}
49
49
  </span>
50
- <span className="shrink-0 text-right font-sans text-sm font-medium leading-[14px] tracking-normal text-storm-gray-900">
50
+ <span className="shrink-0 text-right typography-14 font-medium leading-[14px] tracking-normal text-storm-gray-900">
51
51
  {value}
52
52
  </span>
53
53
  </div>
@@ -57,7 +57,7 @@ function DetailField({ label, value }: { label: string; value: string }) {
57
57
  function DetailsSection({ title, children }: { title: string; children: React.ReactNode }) {
58
58
  return (
59
59
  <section className="flex flex-col gap-3">
60
- <h3 className="font-sans text-xs font-semibold uppercase leading-4 tracking-normal text-storm-gray-200">
60
+ <h3 className="typography-12 font-semibold uppercase tracking-normal text-storm-gray-200">
61
61
  {title}
62
62
  </h3>
63
63
  <div className="flex flex-col gap-4">{children}</div>
@@ -70,7 +70,7 @@ function RequestDetailsBody() {
70
70
  <>
71
71
  <div className="flex flex-col gap-8 pb-6">
72
72
  <div className="flex items-center justify-between gap-4">
73
- <span className="font-sans text-sm font-medium leading-[19px] tracking-normal text-storm-gray-500">
73
+ <span className="typography-14 font-medium tracking-normal text-storm-gray-500">
74
74
  Կարգավիճակ
75
75
  </span>
76
76
  <StatusBadge tone="warning">Սպասում է վճարման</StatusBadge>
@@ -93,7 +93,7 @@ function RequestDetailsBody() {
93
93
 
94
94
  <section className="-mx-6 flex flex-col justify-between border-t border-dashed border-storm-gray-50 px-6 pt-6 pb-0">
95
95
  <div className="flex flex-col gap-3">
96
- <h3 className="font-sans text-xs font-semibold uppercase leading-4 tracking-normal text-storm-gray-200">
96
+ <h3 className="typography-12 font-semibold uppercase tracking-normal text-storm-gray-200">
97
97
  Գործողությունների պատմություն
98
98
  </h3>
99
99
  <HistoryTimeline events={historyEvents} />
@@ -190,7 +190,7 @@ export const WithoutSubtitle: Story = {
190
190
  closeAriaLabel="Փակել"
191
191
  onClose={args.onClose}
192
192
  >
193
- <p className="font-sans text-sm leading-[19px] text-storm-gray-500">
193
+ <p className="typography-14 text-storm-gray-500">
194
194
  Drawer content without a subtitle line in the header.
195
195
  </p>
196
196
  </Drawer>
@@ -205,7 +205,7 @@ function InteractiveDrawerStory() {
205
205
  <WebLayoutCanvas className="min-h-screen bg-storm-gray-100 p-6">
206
206
  <button
207
207
  type="button"
208
- className="rounded-xl border border-storm-gray-50 bg-white px-4 py-2 text-sm font-medium text-slate-blue"
208
+ className="rounded-xl border border-storm-gray-50 bg-white px-4 py-2 typography-14 font-medium text-slate-blue"
209
209
  onClick={() => setOpen(true)}
210
210
  >
211
211
  Բացել drawer-ը
@@ -127,14 +127,14 @@ export function Drawer({
127
127
  <div className="min-w-0 flex flex-col gap-1">
128
128
  <h2
129
129
  id="homepad-drawer-title"
130
- className="font-sans text-[18px] font-bold leading-6 tracking-normal text-black"
130
+ className="typography-title-sm tracking-normal text-black"
131
131
  >
132
132
  {title}
133
133
  </h2>
134
134
  {subtitle ? (
135
135
  <p
136
136
  id="homepad-drawer-subtitle"
137
- className="font-sans text-sm font-normal leading-[19px] tracking-normal text-storm-gray-400"
137
+ className="typography-body tracking-normal text-storm-gray-400"
138
138
  >
139
139
  {subtitle}
140
140
  </p>
@@ -31,10 +31,10 @@ export function HistoryTimeline({ events }: HistoryTimelineProps) {
31
31
  </div>
32
32
 
33
33
  <div className="flex min-w-0 flex-1 flex-col gap-2 pt-1.5">
34
- <p className="font-sans text-sm font-medium leading-[19px] tracking-normal text-slate-blue">
34
+ <p className="typography-14 font-medium tracking-normal text-slate-blue">
35
35
  {event.title}
36
36
  </p>
37
- <p className="font-sans text-xs font-normal leading-4 tracking-normal text-storm-gray-300">
37
+ <p className="typography-caption tracking-normal text-storm-gray-300">
38
38
  {event.timestamp}
39
39
  </p>
40
40
  </div>
@@ -106,7 +106,7 @@ function FooterAlignmentsStory() {
106
106
  <button
107
107
  key={value}
108
108
  type="button"
109
- className="rounded-lg border border-storm-gray-50 px-3 py-1 text-sm"
109
+ className="rounded-lg border border-storm-gray-50 px-3 py-1 typography-14"
110
110
  onClick={() => setAlign(value)}
111
111
  >
112
112
  {value}
@@ -1,4 +1,5 @@
1
1
  import { useEffect, type ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
 
3
4
  import { Button } from '../../components/Button';
4
5
  import { cn } from '../utils/cn';
@@ -22,6 +23,7 @@ export type ModalProps = {
22
23
  cancelDisabled?: boolean;
23
24
  confirmDisabled?: boolean;
24
25
  closeOnBackdrop?: boolean;
26
+ closeOnEscape?: boolean;
25
27
  containerClassName?: string;
26
28
  contentClassName?: string;
27
29
  buttonClassName?: string;
@@ -49,6 +51,7 @@ export function Modal({
49
51
  cancelDisabled = false,
50
52
  confirmDisabled = false,
51
53
  closeOnBackdrop = true,
54
+ closeOnEscape = true,
52
55
  containerClassName,
53
56
  contentClassName,
54
57
  buttonClassName,
@@ -58,8 +61,11 @@ export function Modal({
58
61
  return;
59
62
  }
60
63
 
64
+ const previousOverflow = document.body.style.overflow;
65
+ document.body.style.overflow = 'hidden';
66
+
61
67
  const handleKeyDown = (event: KeyboardEvent) => {
62
- if (event.key === 'Escape') {
68
+ if (event.key === 'Escape' && closeOnEscape) {
63
69
  onCancel();
64
70
  }
65
71
  };
@@ -67,9 +73,10 @@ export function Modal({
67
73
  window.addEventListener('keydown', handleKeyDown);
68
74
 
69
75
  return () => {
76
+ document.body.style.overflow = previousOverflow;
70
77
  window.removeEventListener('keydown', handleKeyDown);
71
78
  };
72
- }, [open, onCancel]);
79
+ }, [open, onCancel, closeOnEscape]);
73
80
 
74
81
  if (!open) {
75
82
  return null;
@@ -81,10 +88,18 @@ export function Modal({
81
88
  footerLayout !== 'split' && buttonClassName,
82
89
  );
83
90
 
84
- return (
91
+ const modal = (
85
92
  <div
86
- className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
87
- onClick={closeOnBackdrop ? onCancel : undefined}
93
+ className="fixed inset-0 z-[100] flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
94
+ onClick={
95
+ closeOnBackdrop
96
+ ? (event) => {
97
+ if (event.target === event.currentTarget) {
98
+ onCancel();
99
+ }
100
+ }
101
+ : undefined
102
+ }
88
103
  role="presentation"
89
104
  >
90
105
  <div
@@ -93,14 +108,14 @@ export function Modal({
93
108
  aria-labelledby="homepad-modal-title"
94
109
  {...(subtitle ? { 'aria-describedby': 'homepad-modal-subtitle' } : {})}
95
110
  className={cn(
96
- 'flex w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
111
+ 'flex max-h-[min(90dvh,calc(100dvh-2rem))] w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
97
112
  containerClassName,
98
113
  )}
99
114
  onClick={(event) => {
100
115
  event.stopPropagation();
101
116
  }}
102
117
  >
103
- <header className={cn('flex', icon ? 'items-center gap-2.5' : undefined)}>
118
+ <header className={cn('flex shrink-0', icon ? 'items-center gap-2.5' : undefined)}>
104
119
  {icon ? (
105
120
  <div
106
121
  className={cn(
@@ -116,14 +131,14 @@ export function Modal({
116
131
  <div className="flex min-w-0 flex-col gap-2">
117
132
  <h2
118
133
  id="homepad-modal-title"
119
- className="font-sans text-lg font-bold leading-6 tracking-normal text-black"
134
+ className="typography-title-sm tracking-normal text-black"
120
135
  >
121
136
  {title}
122
137
  </h2>
123
138
  {subtitle ? (
124
139
  <p
125
140
  id="homepad-modal-subtitle"
126
- className="font-sans text-xs font-normal leading-4 tracking-normal text-storm-gray-400"
141
+ className="typography-caption tracking-normal text-storm-gray-400"
127
142
  >
128
143
  {subtitle}
129
144
  </p>
@@ -131,7 +146,7 @@ export function Modal({
131
146
  </div>
132
147
  </header>
133
148
 
134
- <div className={cn('min-w-0', contentClassName)}>{children}</div>
149
+ <div className={cn('min-h-0 min-w-0 flex-1 overflow-y-auto', contentClassName)}>{children}</div>
135
150
 
136
151
  <footer
137
152
  className={cn(
@@ -159,4 +174,6 @@ export function Modal({
159
174
  </div>
160
175
  </div>
161
176
  );
177
+
178
+ return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
162
179
  }
@@ -23,7 +23,7 @@ const defaultLabels: PaginationLabels = {
23
23
  };
24
24
 
25
25
  const paginationControlClassName =
26
- 'inline-flex size-7 items-center justify-center rounded-lg border border-storm-gray-50 p-1 text-xs font-medium leading-none text-storm-gray-500 transition-colors';
26
+ 'inline-flex size-7 items-center justify-center rounded-lg border border-storm-gray-50 p-1 typography-12 font-medium text-storm-gray-500 transition-colors';
27
27
 
28
28
  const paginationHoverClassName = 'cursor-pointer hover:bg-storm-gray-0';
29
29
 
@@ -78,7 +78,7 @@ export function Pagination({
78
78
  className,
79
79
  )}
80
80
  >
81
- <p className="text-xs font-medium leading-none text-storm-gray-200">
81
+ <p className="typography-12 font-medium text-storm-gray-200">
82
82
  {mergedLabels.showing({ start, end, total })}
83
83
  </p>
84
84
 
@@ -17,7 +17,7 @@ export type TableActionButtonProps = {
17
17
  >;
18
18
 
19
19
  const inlineActionClassName =
20
- 'inline-flex cursor-pointer items-center gap-1 rounded-lg border border-storm-gray-50 bg-white px-3 py-2 font-sans text-xs font-medium leading-none text-storm-gray-900 transition-colors hover:bg-storm-gray-0';
20
+ 'inline-flex cursor-pointer items-center gap-1 rounded-lg border border-storm-gray-50 bg-white px-3 py-2 typography-12 font-medium text-storm-gray-900 transition-colors hover:bg-storm-gray-0';
21
21
 
22
22
  const variantClasses: Record<TableActionButtonVariant, string> = {
23
23
  approve: inlineActionClassName,
@@ -172,7 +172,7 @@ export function TableActionsMenu({
172
172
  item.onSelect();
173
173
  }}
174
174
  className={cn(
175
- 'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left font-sans text-sm font-medium leading-none text-storm-gray-900 transition-colors hover:bg-storm-gray-0',
175
+ 'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium text-storm-gray-900 transition-colors hover:bg-storm-gray-0',
176
176
  index > 0 && 'border-t border-storm-gray-50',
177
177
  item.disabled && 'cursor-not-allowed opacity-50',
178
178
  )}
@@ -12,7 +12,7 @@ export function TableIconText({ icon, children, className }: TableIconTextProps)
12
12
  return (
13
13
  <span
14
14
  className={cn(
15
- 'inline-flex items-center gap-2 font-sans text-sm font-medium leading-none tracking-normal text-storm-gray-900',
15
+ 'inline-flex items-center gap-2 typography-14 font-medium tracking-normal text-storm-gray-900',
16
16
  className,
17
17
  )}
18
18
  >